feat(secretstores): Add Docker secrets (#13035)

This commit is contained in:
Shan Desai 2023-04-25 17:18:30 +02:00 committed by GitHub
parent 77ee21f8e5
commit 74ea83925c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 344 additions and 0 deletions

View File

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

View File

@ -0,0 +1,5 @@
//go:build !custom || secretstores || secretstores.docker
package all
import _ "github.com/influxdata/telegraf/plugins/secretstores/docker" // register plugin

View File

@ -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/

View File

@ -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}
})
}

View File

@ -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")
}

View File

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

View File

@ -0,0 +1 @@
IWontTell

View File

@ -0,0 +1 @@
foobar

View File

@ -0,0 +1 @@
SuperDuperSecret!23