1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,246 @@
# Systemd Secret-Store Plugin
The `systemd` plugin allows utilizing credentials and secrets provided by
[systemd][] to the Telegraf service. Systemd ensures that only the intended
service can access the credentials for the lifetime of this service. The
credentials appear as plaintext files to the consuming service but are stored
encrypted on the host system. This encryption can also use TPM2 protection if
available (see [this article][systemd-descr] for details).
This plugin does not support setting the credentials. See the
[credentials management section](#credential-management) below for how to
setup systemd credentials and how to add credentials
**Note**: Secrets of this plugin are static and are not updated after startup.
## Requirements and caveats
This plugin requires **systemd version 250+** with correctly set-up credentials
via [systemd-creds][] (see [setup section](#credential-management)).
However, to use `ImportCredential`, as done in the default service file, you
need **systemd version 254+** otherwise you need to specify the credentials
using `LoadCredentialEncrypted` in a service-override.
In the default setup, Telegraf expects credential files to be prefixed with
`telegraf.` and without a custom name setting (i.e. no `--name`).
It is important to note that when TPM2 sealing is available on the host,
credentials can only be created and used on the **same machine** as decrypting
the secrets requires the encryption key *and* a key stored in TPM2. Therefore,
creating credentials and then copying it to another machine will fail!
Please be aware that, due to its nature, this plugin is **ONLY** available
when started as a service. It does **NOT** find any credentials when started
manually via the command line! Therefore, `secrets` commands should **not**
be used with this plugin.
## Usage <!-- @/docs/includes/secret_usage.md -->
Secrets defined by a store are referenced with `@{<store-id>:<secret_key>}`
the Telegraf configuration. Only certain Telegraf plugins and options of
support secret stores. To see which plugins and options support
secrets, see their respective documentation (e.g.
`plugins/outputs/influxdb/README.md`). If the plugin's README has the
`Secret-store support` section, it will detail which options support secret
store usage.
## Configuration
```toml @sample.conf
# Secret-store to access systemd secrets
[[secretstores.systemd]]
## 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 = "systemd"
## Path to systemd credentials directory
## This should not be required as systemd indicates this directory
## via the CREDENTIALS_DIRECTORY environment variable.
# path = "${CREDENTIALS_DIRECTORY}"
## Prefix to remove from systemd credential-filenames to derive secret names
# prefix = "telegraf."
```
Each Secret provided by systemd will be available as file under
`${CREDENTIALS_DIRECTORY}/<secret-name>` for the service. You will **not** be
able to see them as a regular, non-telegraf user. Credential visibility from
other systemd services is mediated by the `User=` and `PrivateMounts=`
service-unit directives for those services. See the
[systemd.exec man-page][systemd-exec] for details.
## Credential management
Most steps here are condensed from the [systemd-creds man-page][systemd-creds]
and are using this command. Please also check that man-page as the options
or verbs used here might be outdated for the systemd version you are using.
**Please note**: We are using `/etc/credstore.encrypted` as our storage
location for encrypted credentials throughout the examples below and assuming
a Telegraf install via package manager. If you are using some other means to
install Telegraf you might need to create that directory.
Furthermore, we assume the secret-store ID to be set to `systemd` in the
examples.
Setting up systemd-credentials might vary on your distribution or version so
please also check the documentation there. You might also need to install
supporting packages such as `tpm2-tools`.
### Setup
If you have not done it already, systemd requires a first-time setup of the
credential system. If you are planning to use the TPM2 chip of your system
for protecting the credentials, you should first make sure that it is
available using
```shell
sudo systemd-creds has-tpm2
```
The output should look similar to
```text
partial
-firmware
+driver
+system
+subsystem
```
If TPM2 is available on your system, credentials can also be tied to the device
by utilizing TPM2 sealing. See the [systemd-creds man-page][systemd-creds] for
details.
Now setup the credentials by creating the root key.
```shell
sudo systemd-creds setup
```
A warning may appears if you are storing the generated key on an unencrypted
disk which is not recommended. With this, we are all set to create credentials.
### Creating credentials
After setting up the encryption key we can create a new credential using
```shell
echo -n "john-doe-jr" | sudo systemd-creds encrypt - /etc/credstore.encrypted/telegraf.http_user
```
You should now have a file named `telegraf.http_user` containing the encrypted
username. The secret-store later provides the secret using this filename as the
secret's key.
**Please note:**: By default Telegraf strips the `telegraf.` prefix. If you use
a different prefix or no prefix at all you need to adapt the `prefix` setting!
We can now add more secrets. To avoid potentially leaking the plain-text
credentials through command-history or showing it on the screen we use
```shell
systemd-ask-password -n | sudo systemd-creds encrypt - /etc/credstore.encrypted/telegraf.http_password
```
to interactively enter the password.
### Using credentials as secrets
To use the credentials as secrets you need to first instantiate a `systemd`
secret-store by adding
```toml
[[secretstores.systemd]]
id = "systemd"
```
to your Telegraf configuration. Assuming the two example credentials
`http_user` and `http_password` you can now use those as secrets via
```toml
[[inputs.http]]
urls = ["http://localhost/metrics"]
username = "@{systemd:http_user}"
password = "@{systemd:http_password}"
```
in your plugins.
### Chaining for unattended start
When using many secrets or when secrets need to be shared among hosts, listing
all of them in the service file might be cumbersome. Additionally, it is hard
to manually test Telegraf configurations with the `systemd` secret-store as
those secrets are only available when started as a service.
Here, secret-store chaining comes into play, denoting a setup where one
secret-store, in our case `secretstores.systemd`, is used to unlock another
secret-store (`secretstores.jose` in this example).
```toml
[[secretstores.systemd]]
id = "systemd"
[[secretstores.jose]]
id = "mysecrets"
path = "/etc/telegraf/secrets"
password = "@{systemd:initial}"
```
Here we assume that an `initial` credential was injected through the service
file. This `initial` secret is then used to unlock the `jose` secret-store
which might provide many different secrets backed by encrypted files.
Input and output plugins can the use the `jose` secrets (via `@{mysecrets:...}`)
to fill sensitive data such as usernames, passwords or tokens.
### Troubleshooting
Please always make sure your systemd version matches Telegraf's requirements,
i.e. you do have version 254 or later.
When not being able to start the service please check the logs. A common issue
is using the `--name` option which does not work with systemd's
`ImportCredential` setting.
a mismatch between the name stored in the credential (given during
`systemd-creds encrypt`) and the one used in the
`LoadCredentialEncrypted` statement.
In case you are having trouble referencing credentials in Telegraf, you should
check what is available via
```shell
CREDENTIALS_DIRECTORY=/etc/credstore.encrypted sudo systemd-creds list
```
for the example above you should see
```text
NAME SECURE SIZE PATH
-------------------------------------------------------------------
telegraf.http_password insecure 146B /etc/credstore.encrypted/telegraf.http_password
telegraf.http_user insecure 142B /etc/credstore.encrypted/telegraf.http_user
```
**Please note**: Telegraf's secret management functionality is not helpful here
as credentials are *only* available to the systemd service, not via commandline.
Remember to remove the `prefix` configured in your secret-store from the `NAME`
column to get the secrets' `key`.
To get the actual value of a credential use
```shell
sudo systemd-creds decrypt /etc/credstore.encrypted/telegraf.http_password -
```
Please use the above command(s) with care as they do reveal the secret value
of the credential!
[systemd]: https://www.freedesktop.org/wiki/Software/systemd/
[systemd-descr]: https://systemd.io/CREDENTIALS
[systemd-creds]: https://www.freedesktop.org/software/systemd/man/systemd-creds.html
[systemd-exec]: https://www.freedesktop.org/software/systemd/man/systemd.exec.html

View file

@ -0,0 +1,15 @@
# Secret-store to access systemd secrets
[[secretstores.systemd]]
## 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 = "systemd"
## Path to systemd credentials directory
## This should not be required as systemd indicates this directory
## via the CREDENTIALS_DIRECTORY environment variable.
# path = "${CREDENTIALS_DIRECTORY}"
## Prefix to remove from systemd credential-filenames to derive secret names
# prefix = "telegraf."

View file

@ -0,0 +1,139 @@
//go:build linux
//go:generate ../../../tools/readme_config_includer/generator
package systemd
import (
"context"
_ "embed"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/coreos/go-systemd/v22/dbus"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/secretstores"
)
const systemdMinimumVersion = 250
// Required to be a variable to mock the version in tests
var getSystemdVersion = getSystemdMajorVersion
//go:embed sample.conf
var sampleConfig string
type Systemd struct {
Path string `toml:"path"`
Prefix string `toml:"prefix"`
Log telegraf.Logger `toml:"-"`
}
func (*Systemd) SampleConfig() string {
return sampleConfig
}
// Init initializes all internals of the secret-store
func (s *Systemd) Init() error {
version, err := getSystemdVersion()
if err != nil {
return fmt.Errorf("unable to detect systemd version: %w", err)
}
s.Log.Debugf("Found systemd version %d...", version)
if version < systemdMinimumVersion {
return fmt.Errorf("systemd version %d below minimum version %d", version, systemdMinimumVersion)
}
// By default the credentials directory is passed in by systemd
// via the "CREDENTIALS_DIRECTORY" environment variable.
defaultPath := os.Getenv("CREDENTIALS_DIRECTORY")
if defaultPath == "" {
s.Log.Warn("CREDENTIALS_DIRECTORY environment variable undefined. Make sure credentials are setup correctly!")
if s.Path == "" {
return errors.New("'path' required without CREDENTIALS_DIRECTORY")
}
}
// Use default path if no explicit was specified. This should be the common case.
if s.Path == "" {
s.Path = defaultPath
}
s.Path, err = filepath.Abs(s.Path)
if err != nil {
return fmt.Errorf("cannot determine absolute path of %q: %w", s.Path, err)
}
// Check if we can access the target directory
if _, err := os.Stat(s.Path); err != nil {
return fmt.Errorf("accessing credentials directory %q failed: %w", s.Path, err)
}
return nil
}
func (s *Systemd) Get(key string) ([]byte, error) {
secretFile, err := filepath.Abs(filepath.Join(s.Path, s.Prefix+key))
if err != nil {
return nil, err
}
if filepath.Dir(secretFile) != s.Path {
return nil, fmt.Errorf("invalid directory detected for key %q", key)
}
value, err := os.ReadFile(secretFile)
if err != nil {
return nil, fmt.Errorf("cannot read the secret's value: %w", err)
}
return value, nil
}
func (s *Systemd) List() ([]string, error) {
secretFiles, err := os.ReadDir(s.Path)
if err != nil {
return nil, fmt.Errorf("cannot read files: %w", err)
}
secrets := make([]string, 0, len(secretFiles))
for _, entry := range secretFiles {
key := strings.TrimPrefix(entry.Name(), s.Prefix)
secrets = append(secrets, key)
}
return secrets, nil
}
func (*Systemd) Set(_, _ string) error {
return errors.New("secret-store does not support creating secrets")
}
// GetResolver returns a function to resolve the given key.
func (s *Systemd) GetResolver(key string) (telegraf.ResolveFunc, error) {
resolver := func() ([]byte, bool, error) {
s, err := s.Get(key)
return s, false, err
}
return resolver, nil
}
func getSystemdMajorVersion() (int, error) {
ctx := context.Background()
conn, err := dbus.NewWithContext(ctx)
if err != nil {
return 0, err
}
defer conn.Close()
fullVersion, err := conn.GetManagerProperty("Version")
if err != nil {
return 0, err
}
fullVersion = strings.Trim(fullVersion, "\"")
return strconv.Atoi(strings.SplitN(fullVersion, ".", 2)[0])
}
// Register the secret-store on load.
func init() {
secretstores.Add("systemd", func(_ string) telegraf.SecretStore {
return &Systemd{Prefix: "telegraf."}
})
}

View file

@ -0,0 +1 @@
package systemd

View file

@ -0,0 +1,175 @@
//go:build linux
package systemd
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/testutil"
)
func getSystemdVersionMin() (int, error) {
return systemdMinimumVersion, nil
}
func TestSampleConfig(t *testing.T) {
plugin := &Systemd{}
require.NotEmpty(t, plugin.SampleConfig())
}
func TestMinimumVersion(t *testing.T) {
getSystemdVersion = func() (int, error) { return 123, nil }
plugin := &Systemd{Log: testutil.Logger{}}
require.ErrorContains(t, plugin.Init(), "below minimum version")
}
func TestEmptyPath(t *testing.T) {
getSystemdVersion = getSystemdVersionMin
plugin := &Systemd{Log: testutil.Logger{}}
require.ErrorContains(t, plugin.Init(), "'path' required without CREDENTIALS_DIRECTORY")
}
func TestEmptyCredentialsDirectoryWarning(t *testing.T) {
getSystemdVersion = getSystemdVersionMin
logger := &testutil.CaptureLogger{}
plugin := &Systemd{
Path: "testdata",
Log: logger}
require.NoError(t, plugin.Init())
actual := logger.Warnings()
require.Len(t, actual, 1)
require.Contains(t, actual[0], "CREDENTIALS_DIRECTORY environment variable undefined")
}
func TestPathNonExistentExplicit(t *testing.T) {
getSystemdVersion = getSystemdVersionMin
t.Setenv("CREDENTIALS_DIRECTORY", "testdata")
plugin := &Systemd{
Path: "non/existent/path",
Log: testutil.Logger{},
}
require.ErrorContains(t, plugin.Init(), "accessing credentials directory")
}
func TestPathNonExistentImplicit(t *testing.T) {
getSystemdVersion = getSystemdVersionMin
t.Setenv("CREDENTIALS_DIRECTORY", "non/existent/path")
plugin := &Systemd{
Log: testutil.Logger{},
}
require.ErrorContains(t, plugin.Init(), "accessing credentials directory")
}
func TestInit(t *testing.T) {
getSystemdVersion = getSystemdVersionMin
t.Setenv("CREDENTIALS_DIRECTORY", "testdata")
plugin := &Systemd{Log: testutil.Logger{}}
require.NoError(t, plugin.Init())
}
func TestSetNotAvailable(t *testing.T) {
getSystemdVersion = getSystemdVersionMin
t.Setenv("CREDENTIALS_DIRECTORY", "testdata")
plugin := &Systemd{Log: testutil.Logger{}}
require.NoError(t, plugin.Init())
// Try to Store the secrets, which this plugin should not let
require.ErrorContains(t, plugin.Set("foo", "bar"), "secret-store does not support creating secrets")
}
func TestListGet(t *testing.T) {
getSystemdVersion = getSystemdVersionMin
t.Setenv("CREDENTIALS_DIRECTORY", "testdata")
// 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",
}
// Initialize the plugin
plugin := &Systemd{Log: testutil.Logger{}}
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) {
getSystemdVersion = getSystemdVersionMin
t.Setenv("CREDENTIALS_DIRECTORY", "testdata")
// Secret Value Name to Resolve
secretFileName := "secret-file-1"
// Secret Value to Resolve To
secretVal := "IWontTell"
// Initialize the plugin
plugin := &Systemd{Log: testutil.Logger{}}
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) {
getSystemdVersion = getSystemdVersionMin
t.Setenv("CREDENTIALS_DIRECTORY", "testdata")
// Initialize the plugin
plugin := &Systemd{Log: testutil.Logger{}}
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:")
}
func TestGetNonExistent(t *testing.T) {
getSystemdVersion = getSystemdVersionMin
t.Setenv("CREDENTIALS_DIRECTORY", "testdata")
// Initialize the plugin
plugin := &Systemd{Log: testutil.Logger{}}
require.NoError(t, plugin.Init())
// Get the resolver
_, err := plugin.Get("foo")
require.ErrorContains(t, err, "cannot read the secret's value:")
}

View file

@ -0,0 +1 @@
IWontTell

View file

@ -0,0 +1 @@
foobar

View file

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