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
|
||||
* 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