feat(secretstores): Add Docker secrets (#13035)
This commit is contained in:
parent
77ee21f8e5
commit
74ea83925c
|
|
@ -4,3 +4,4 @@ This folder contains the plugins for the secret-store functionality:
|
||||||
|
|
||||||
* jose: Javascript Object Signing and Encryption
|
* jose: Javascript Object Signing and Encryption
|
||||||
* os: Native tooling provided on Linux, MacOS, or Windows.
|
* os: Native tooling provided on Linux, MacOS, or Windows.
|
||||||
|
* docker: Docker Secrets within containers
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
//go:build !custom || secretstores || secretstores.docker
|
||||||
|
|
||||||
|
package all
|
||||||
|
|
||||||
|
import _ "github.com/influxdata/telegraf/plugins/secretstores/docker" // register plugin
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
# Docker Secrets Secret-Store Plugin
|
||||||
|
|
||||||
|
The `docker` plugin allows to utilize credentials and secrets mounted by
|
||||||
|
Docker during container runtime. The secrets are mounted as files
|
||||||
|
under the `/run/secrets` directory within the container.
|
||||||
|
|
||||||
|
> NOTE: This plugin can ONLY read the mounted secrets from Docker and NOT set them.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```toml @sample.conf
|
||||||
|
# Secret-store to access Docker Secrets
|
||||||
|
[[secretstores.docker]]
|
||||||
|
## Unique identifier for the secretstore.
|
||||||
|
## This id can later be used in plugins to reference the secrets
|
||||||
|
## in this secret-store via @{<id>:<secret_key>} (mandatory)
|
||||||
|
id = "docker_secretstore"
|
||||||
|
|
||||||
|
## Default Path to directory where docker stores the secrets file
|
||||||
|
## Current implementation in docker compose v2 only allows the following
|
||||||
|
## value for the path where the secrets are mounted at runtime
|
||||||
|
# path = "/run/secrets"
|
||||||
|
|
||||||
|
## Allow dynamic secrets that are updated during runtime of telegraf
|
||||||
|
## Dynamic Secrets work only with `file` or `external` configuration
|
||||||
|
## in `secrets` section of the `docker-compose.yml` file
|
||||||
|
# dynamic = false
|
||||||
|
```
|
||||||
|
|
||||||
|
Each Secret mentioned within a Compose service's `secrets` parameter will be
|
||||||
|
available as file under the `/run/secrets/<secret-name>` within the container.
|
||||||
|
|
||||||
|
It is possible to let Telegraf pick changed secret values into plugins by setting
|
||||||
|
`dynamic = true`. This feature will work only for Docker Secrets provided via
|
||||||
|
`file` and `external` type within the `docker-compose.yml` file
|
||||||
|
and not when using `environment` type
|
||||||
|
(Refer here [Docker Secrets in Compose Specification][1]).
|
||||||
|
|
||||||
|
## Example Compose File
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
telegraf:
|
||||||
|
image: docker.io/telegraf:latest
|
||||||
|
container_name: dockersecret_telegraf
|
||||||
|
user: "${USERID}" # Required to access the /run/secrets directory in container
|
||||||
|
secrets:
|
||||||
|
- secret_for_plugin
|
||||||
|
volumes:
|
||||||
|
- /path/to/telegrafconf/host:/etc/telegraf/telegraf.conf:ro
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
secret_for_plugin:
|
||||||
|
environment: TELEGRAF_PLUGIN_CREDENTIAL
|
||||||
|
```
|
||||||
|
|
||||||
|
here the `TELEGRAF_PLUGIN_CREDENTIAL` exists in a `.env` file in the same directory
|
||||||
|
as the `docker-compose.yml`. An example of the `.env` file can be as follows:
|
||||||
|
|
||||||
|
```env
|
||||||
|
TELEGRAF_PLUGIN_CREDENTIAL=superSecretStuff
|
||||||
|
# determine this value by executing `id -u` in terminal
|
||||||
|
USERID=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Referencing Secret within a Plugin
|
||||||
|
|
||||||
|
Referencing the secret within a plugin occurs by:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[inputs.<some_plugin>]]
|
||||||
|
password = "@{docker_secretstore:secret_for_plugin}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additonal Information
|
||||||
|
|
||||||
|
[Docker Secrets in Swarm][2]
|
||||||
|
|
||||||
|
[Creating Secrets in Docker][3]
|
||||||
|
|
||||||
|
[1]: https://github.com/compose-spec/compose-spec/blob/master/09-secrets.md
|
||||||
|
[2]: https://docs.docker.com/engine/swarm/secrets/
|
||||||
|
[3]: https://www.rockyourcode.com/using-docker-secrets-with-docker-compose/
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
//go:generate ../../../tools/readme_config_includer/generator
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/influxdata/telegraf"
|
||||||
|
"github.com/influxdata/telegraf/plugins/secretstores"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed sample.conf
|
||||||
|
var sampleConfig string
|
||||||
|
|
||||||
|
type Docker struct {
|
||||||
|
ID string `toml:"id"`
|
||||||
|
Path string `toml:"path"`
|
||||||
|
Dynamic bool `toml:"dynamic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Docker) SampleConfig() string {
|
||||||
|
return sampleConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes all internals of the secret-store
|
||||||
|
func (d *Docker) Init() error {
|
||||||
|
if d.ID == "" {
|
||||||
|
return errors.New("id missing")
|
||||||
|
}
|
||||||
|
if d.Path == "" {
|
||||||
|
// setting the default directory for Docker Secrets
|
||||||
|
// if no explicit path mentioned in configuration
|
||||||
|
d.Path = "/run/secrets"
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(d.Path); err != nil {
|
||||||
|
// if there is no /run/secrets directory for default Path value
|
||||||
|
// this implies that there are no secrets.
|
||||||
|
// Or for any explicit path definitions for that matter.
|
||||||
|
return fmt.Errorf("accessing directory %q failed: %w", d.Path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Docker) Get(key string) ([]byte, error) {
|
||||||
|
secretFile, err := filepath.Abs(filepath.Join(d.Path, key))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if filepath.Dir(secretFile) != d.Path {
|
||||||
|
return nil, fmt.Errorf("directory traversal detected for key %q", key)
|
||||||
|
}
|
||||||
|
value, err := os.ReadFile(secretFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot read the secret's value under the directory: %w", err)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Docker) List() ([]string, error) {
|
||||||
|
secretFiles, err := os.ReadDir(d.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot read files under the directory: %w", err)
|
||||||
|
}
|
||||||
|
secrets := make([]string, 0, len(secretFiles))
|
||||||
|
for _, entry := range secretFiles {
|
||||||
|
secrets = append(secrets, entry.Name())
|
||||||
|
}
|
||||||
|
return secrets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Docker) Set(_, _ string) error {
|
||||||
|
return errors.New("secret-store does not support creating secrets")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResolver returns a function to resolve the given key.
|
||||||
|
func (d *Docker) GetResolver(key string) (telegraf.ResolveFunc, error) {
|
||||||
|
resolver := func() ([]byte, bool, error) {
|
||||||
|
s, err := d.Get(key)
|
||||||
|
return s, d.Dynamic, err
|
||||||
|
}
|
||||||
|
return resolver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the secret-store on load.
|
||||||
|
func init() {
|
||||||
|
secretstores.Add("docker", func(id string) telegraf.SecretStore {
|
||||||
|
return &Docker{ID: id}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSampleConfig(t *testing.T) {
|
||||||
|
plugin := &Docker{}
|
||||||
|
require.NotEmpty(t, plugin.SampleConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitFail(t *testing.T) {
|
||||||
|
plugin := &Docker{}
|
||||||
|
require.ErrorContains(t, plugin.Init(), "id missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathNonExistant(t *testing.T) {
|
||||||
|
plugin := &Docker{
|
||||||
|
ID: "non_existent_path_test",
|
||||||
|
Path: "non/existent/path",
|
||||||
|
}
|
||||||
|
require.ErrorContainsf(t, plugin.Init(), "accessing directory", "accessing directory %q failed: %w", plugin.Path, plugin.Init())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetNotAvailable(t *testing.T) {
|
||||||
|
testdir, err := filepath.Abs("testdata")
|
||||||
|
require.NoError(t, err, "testdata cannot be found")
|
||||||
|
|
||||||
|
plugin := &Docker{
|
||||||
|
ID: "set_path_test",
|
||||||
|
Path: testdir,
|
||||||
|
}
|
||||||
|
require.NoError(t, plugin.Init())
|
||||||
|
|
||||||
|
// Try to Store the secrets, which this plugin should not let
|
||||||
|
secret := map[string]string{
|
||||||
|
"secret-file-1": "TryToSetThis",
|
||||||
|
}
|
||||||
|
for k, v := range secret {
|
||||||
|
require.ErrorContains(t, plugin.Set(k, v), "secret-store does not support creating secrets")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListGet(t *testing.T) {
|
||||||
|
// secret files name and their content to compare under the `testdata` directory
|
||||||
|
secrets := map[string]string{
|
||||||
|
"secret-file-1": "IWontTell",
|
||||||
|
"secret_file_2": "SuperDuperSecret!23",
|
||||||
|
"secretFile": "foobar",
|
||||||
|
}
|
||||||
|
|
||||||
|
testdir, err := filepath.Abs("testdata")
|
||||||
|
require.NoError(t, err, "testdata cannot be found")
|
||||||
|
|
||||||
|
// Initialize the plugin
|
||||||
|
plugin := &Docker{
|
||||||
|
ID: "test_list_get",
|
||||||
|
Path: testdir,
|
||||||
|
}
|
||||||
|
require.NoError(t, plugin.Init())
|
||||||
|
|
||||||
|
// List the Secrets
|
||||||
|
keys, err := plugin.List()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, keys, len(secrets))
|
||||||
|
// check if the returned array from List() is the same
|
||||||
|
// as the name of secret files
|
||||||
|
for secretFileName := range secrets {
|
||||||
|
require.Contains(t, keys, secretFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the secrets
|
||||||
|
for _, k := range keys {
|
||||||
|
value, err := plugin.Get(k)
|
||||||
|
require.NoError(t, err)
|
||||||
|
v, found := secrets[k]
|
||||||
|
require.Truef(t, found, "unexpected secret requested that was not found: %q", k)
|
||||||
|
require.Equal(t, v, string(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolver(t *testing.T) {
|
||||||
|
// Secret Value Name to Resolve
|
||||||
|
secretFileName := "secret-file-1"
|
||||||
|
// Secret Value to Resolve To
|
||||||
|
secretVal := "IWontTell"
|
||||||
|
|
||||||
|
testdir, err := filepath.Abs("testdata")
|
||||||
|
require.NoError(t, err, "testdata cannot be found")
|
||||||
|
|
||||||
|
// Initialize the plugin
|
||||||
|
plugin := &Docker{
|
||||||
|
ID: "test_resolver",
|
||||||
|
Path: testdir,
|
||||||
|
}
|
||||||
|
require.NoError(t, plugin.Init())
|
||||||
|
|
||||||
|
// Get the resolver
|
||||||
|
resolver, err := plugin.GetResolver(secretFileName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resolver)
|
||||||
|
s, dynamic, err := resolver()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, dynamic)
|
||||||
|
require.Equal(t, secretVal, string(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverInvalid(t *testing.T) {
|
||||||
|
testdir, err := filepath.Abs("testdata")
|
||||||
|
require.NoError(t, err, "testdata cannot be found")
|
||||||
|
|
||||||
|
// Initialize the plugin
|
||||||
|
plugin := &Docker{
|
||||||
|
ID: "test_invalid_resolver",
|
||||||
|
Path: testdir,
|
||||||
|
}
|
||||||
|
require.NoError(t, plugin.Init())
|
||||||
|
|
||||||
|
// Get the resolver
|
||||||
|
resolver, err := plugin.GetResolver("foo")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resolver)
|
||||||
|
_, _, err = resolver()
|
||||||
|
require.ErrorContains(t, err, "cannot read the secret's value under the directory:")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNonExistant(t *testing.T) {
|
||||||
|
testdir, err := filepath.Abs("testdata")
|
||||||
|
require.NoError(t, err, "testdata cannot be found")
|
||||||
|
|
||||||
|
// Initialize the plugin
|
||||||
|
plugin := &Docker{
|
||||||
|
ID: "test_nonexistent_get",
|
||||||
|
Path: testdir,
|
||||||
|
}
|
||||||
|
require.NoError(t, plugin.Init())
|
||||||
|
|
||||||
|
// Get the resolver
|
||||||
|
_, err = plugin.Get("foo")
|
||||||
|
require.ErrorContains(t, err, "cannot read the secret's value under the directory")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Secret-store to access Docker Secrets
|
||||||
|
[[secretstores.docker]]
|
||||||
|
## Unique identifier for the secretstore.
|
||||||
|
## This id can later be used in plugins to reference the secrets
|
||||||
|
## in this secret-store via @{<id>:<secret_key>} (mandatory)
|
||||||
|
id = "docker_secretstore"
|
||||||
|
|
||||||
|
## Default Path to directory where docker stores the secrets file
|
||||||
|
## Current implementation in docker compose v2 only allows the following
|
||||||
|
## value for the path where the secrets are mounted at runtime
|
||||||
|
# path = "/run/secrets"
|
||||||
|
|
||||||
|
## Allow dynamic secrets that are updated during runtime of telegraf
|
||||||
|
## Dynamic Secrets work only with `file` or `external` configuration
|
||||||
|
## in `secrets` section of the `docker-compose.yml` file
|
||||||
|
# dynamic = false
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
IWontTell
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
foobar
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
SuperDuperSecret!23
|
||||||
Loading…
Reference in New Issue