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,175 @@
# HTTP Secret-store Plugin
The `http` plugin allows to query secrets from an HTTP endpoint. The secrets
can be transmitted plain-text or in an encrypted fashion.
To manage your secrets of this secret-store, you should use Telegraf. Run
```shell
telegraf secrets help
```
to get more information on how to do this.
## 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
# Read secrets from a HTTP endpoint
[[secretstores.http]]
## Unique identifier for the secret-store.
## This id can later be used in plugins to reference the secrets
## in this secret-store via @{<id>:<secret_key>} (mandatory)
id = "secretstore"
## URLs from which to read the secrets
url = "http://localhost/secrets"
## Optional HTTP headers
# headers = {"X-Special-Header" = "Special-Value"}
## Optional Token for Bearer Authentication via
## "Authorization: Bearer <token>" header
# token = "your-token"
## Optional Credentials for HTTP Basic Authentication
# username = "username"
# password = "pa$$word"
## OAuth2 Client Credentials. The options 'client_id', 'client_secret', and 'token_url' are required to use OAuth2.
# client_id = "clientid"
# client_secret = "secret"
# token_url = "https://indentityprovider/oauth2/v1/token"
# scopes = ["urn:opc:idm:__myscopes__"]
## HTTP Proxy support
# use_system_proxy = false
# http_proxy_url = ""
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
## Minimal TLS version to accept by the client
# tls_min_version = "TLS12"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false
## Optional Cookie authentication
# cookie_auth_url = "https://localhost/authMe"
# cookie_auth_method = "POST"
# cookie_auth_username = "username"
# cookie_auth_password = "pa$$word"
# cookie_auth_headers = { Content-Type = "application/json", X-MY-HEADER = "hello" }
# cookie_auth_body = '{"username": "user", "password": "pa$$word", "authenticate": "me"}'
## When unset or set to zero the authentication will only happen once
## and will never renew the cookie. Set to a suitable duration if you
## require cookie renewal!
# cookie_auth_renewal = "0s"
## Amount of time allowed to complete the HTTP request
# timeout = "5s"
## List of success status codes
# success_status_codes = [200]
## JSONata expression to transform the server response into a
## { "secret name": "secret value", ... }
## form. See https://jsonata.org for more information and a playground.
# transformation = ''
## Cipher used to decrypt the secrets.
## In case your secrets are transmitted in an encrypted form, you need
## to specify the cipher used and provide the corresponding configuration.
## Please refer to https://github.com/influxdata/telegraf/blob/master/plugins/secretstores/http/README.md
## for supported values.
# cipher = "none"
## AES cipher parameters
# [secretstores.http.aes]
# ## Key (hex-encoded) and initialization-vector (IV) for the decryption.
# ## In case the key (and IV) is derived from a password, the values can
# ## be omitted.
# key = ""
# init_vector = ""
#
# ## Parameters for password-based-key derivation.
# ## These parameters must match the encryption side to derive the same
# ## key on both sides!
# # kdf_algorithm = "PBKDF2-HMAC-SHA256"
# # password = ""
# # salt = ""
# # iterations = 0
```
A collection of secrets is queried from the `url` endpoint. The plugin currently
expects JSON data in a flat key-value form and means to convert arbitrary JSON
to that form (see [transformation section](#transformation)).
Furthermore, the secret data can be transmitted in an encrypted
format, see [encryption section](#encryption) for details.
## Transformation
Secrets are currently expected to be JSON data in the following flat key-value
form
```json
{
"secret name A": "secret value A",
...
"secret name X": "secret value X"
}
```
If your HTTP endpoint provides JSON data in a different format, you can use
the `transformation` option to apply a [JSONata expression](https://jsonata.org)
(version v1.5.4) to transform the server answer to the above format.
## Encryption
### Plain text
Set `cipher` to `none` if the secrets are transmitted as plain-text. No further
options are required.
### Advanced Encryption Standard (AES)
Currently the following AES ciphers are supported
- `AES128/CBC`: 128-bit key in _CBC_ block mode without padding
- `AES128/CBC/PKCS#5`: 128-bit key in _CBC_ block mode with _PKCS#5_ padding
- `AES128/CBC/PKCS#7`: 128-bit key in _CBC_ block mode with _PKCS#7_ padding
- `AES192/CBC`: 192-bit key in _CBC_ block mode without padding
- `AES192/CBC/PKCS#5`: 192-bit key in _CBC_ block mode with _PKCS#5_ padding
- `AES192/CBC/PKCS#7`: 192-bit key in _CBC_ block mode with _PKCS#7_ padding
- `AES256/CBC`: 256-bit key in _CBC_ block mode without padding
- `AES256/CBC/PKCS#5`: 256-bit key in _CBC_ block mode with _PKCS#5_ padding
- `AES256/CBC/PKCS#7`: 256-bit key in _CBC_ block mode with _PKCS#7_ padding
Additional to the cipher, you need to provide the encryption `key` and
initialization vector `init_vector` to be able to decrypt the data.
In case you are using password-based key derivation, `key`
(and possibly `init_vector`) can be omitted. Take a look at the
[password-based key derivation section](#password-based-key-derivation).
### Password-based key derivation
Alternatively to providing a `key` (and `init_vector`) the key (and vector)
can be derived from a given password. Currently the following algorithms are
supported for `kdf_algorithm`:
- `PBKDF2-HMAC-SHA256` for `key` only, no `init_vector` created
You also need to provide the `password` to derive the key from as well as the
`salt` and `iterations` used.
__Please note:__ All parameters must match the encryption side to derive the
same key in Telegraf!

View file

@ -0,0 +1,172 @@
package http
import (
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"errors"
"fmt"
"strings"
"github.com/awnumar/memguard"
"github.com/influxdata/telegraf/config"
)
type AesEncryptor struct {
Variant []string `toml:"-"`
Key config.Secret `toml:"key"`
Vec config.Secret `toml:"init_vector"`
KDFConfig
mode string
trim func([]byte) ([]byte, error)
}
func (a *AesEncryptor) Init() error {
var cipherName, mode, padding string
switch len(a.Variant) {
case 3:
padding = strings.ToLower(a.Variant[2])
fallthrough
case 2:
mode = strings.ToLower(a.Variant[1])
cipherName = strings.ToLower(a.Variant[0])
if !strings.HasPrefix(cipherName, "aes") {
return fmt.Errorf("requested AES but specified %q", cipherName)
}
case 1:
return errors.New("please specify cipher mode")
case 0:
return errors.New("please specify cipher")
default:
return errors.New("too many variant elements")
}
var keylen int
switch cipherName {
case "aes128":
keylen = 16
case "aes192":
keylen = 24
case "aes256":
keylen = 32
default:
return fmt.Errorf("unsupported AES cipher %q", cipherName)
}
if mode != "cbc" {
return fmt.Errorf("unsupported cipher mode %q", a.Variant[1])
}
a.mode = mode
// Setup the trimming function to revert padding
switch padding {
case "", "none":
// identity, no padding
a.trim = func(in []byte) ([]byte, error) { return in, nil }
case "pkcs#5", "pkcs#7":
a.trim = PKCS5or7Trimming
default:
return fmt.Errorf("unsupported padding %q", padding)
}
// Generate the key using password-based-keys
if a.Key.Empty() {
if a.Passwd.Empty() {
return errors.New("either key or password has to be specified")
}
if a.Salt.Empty() || a.Iterations == 0 {
return errors.New("salt and iterations required for password-based-keys")
}
key, iv, err := a.KDFConfig.NewKey(keylen)
if err != nil {
return fmt.Errorf("generating key failed: %w", err)
}
a.Key.Destroy()
a.Key = key
if a.Vec.Empty() && !iv.Empty() {
a.Vec.Destroy()
a.Vec = iv
}
} else {
encodedKey, err := a.Key.Get()
if err != nil {
return fmt.Errorf("getting key failed: %w", err)
}
key := make([]byte, hex.DecodedLen(len(encodedKey.Bytes())))
_, err = hex.Decode(key, encodedKey.Bytes())
encodedKey.Destroy()
if err != nil {
return fmt.Errorf("decoding key failed: %w", err)
}
actuallen := len(key)
memguard.WipeBytes(key)
if actuallen != keylen {
return fmt.Errorf("key length (%d bit) does not match cipher (%d bit)", actuallen*8, keylen*8)
}
}
if a.Vec.Empty() {
return errors.New("'init_vector' has to be specified or derived from password")
}
encodedIV, err := a.Vec.Get()
if err != nil {
return fmt.Errorf("getting IV failed: %w", err)
}
ivlen := len(encodedIV.Bytes())
encodedIV.Destroy()
if ivlen != 2*aes.BlockSize {
return errors.New("init vector size must match block size")
}
return nil
}
func (a *AesEncryptor) Decrypt(data []byte) ([]byte, error) {
if len(data)%aes.BlockSize != 0 {
return nil, fmt.Errorf("invalid data size %d", len(data))
}
if a.mode != "cbc" {
return nil, fmt.Errorf("unsupported cipher mode %q", a.mode)
}
// Setup the cipher and return the decoded data
encodedKey, err := a.Key.Get()
if err != nil {
return nil, fmt.Errorf("getting key failed: %w", err)
}
key := make([]byte, hex.DecodedLen(len(encodedKey.Bytes())))
_, err = hex.Decode(key, encodedKey.Bytes())
encodedKey.Destroy()
if err != nil {
return nil, fmt.Errorf("decoding key failed: %w", err)
}
// Setup AES
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("creating AES cipher failed: %w", err)
}
// Setup the block/stream cipher and decode the data
encodedIV, err := a.Vec.Get()
if err != nil {
return nil, fmt.Errorf("getting initialization-vector failed: %w", err)
}
iv := make([]byte, hex.DecodedLen(len(encodedIV.Bytes())))
_, err = hex.Decode(iv, encodedIV.Bytes())
encodedIV.Destroy()
if err != nil {
memguard.WipeBytes(iv)
return nil, fmt.Errorf("decoding init vector failed: %w", err)
}
cipher.NewCBCDecrypter(block, iv).CryptBlocks(data, data)
return a.trim(data)
}

View file

@ -0,0 +1,319 @@
package http
import (
"encoding/hex"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/config"
)
func TestAES(t *testing.T) {
keySource := hex.EncodeToString([]byte("0123456789abcdefghijklmnopqrstuvwxyz"))
expected := "my $ecret-Passw0rd"
iv := hex.EncodeToString([]byte("0123456789abcdef"))
tests := []struct {
cipher string
encrypted string
key string
}{
{
cipher: "AES128/CBC/PKCS#5",
encrypted: "9E36B490B0B1D6CE28550DF9DE65FC0013FF9F0939E24DA4A24324BDB5EABA04",
key: keySource[:32],
},
{
cipher: "AES192/CBC/PKCS#5",
encrypted: "D3A5A0004B6783351F89B00C1D4154EDF2321EDAD3111B5551C18836B9FCFD62",
key: keySource[:48],
},
{
cipher: "AES256/CBC/PKCS#5",
encrypted: "9751D7FB4B1497DEBC8A95C5D88097ECB1B8E63979E2D41E7ECD304D6B39B808",
key: keySource[:64],
},
}
for _, tt := range tests {
t.Run(tt.cipher, func(t *testing.T) {
decrypter := AesEncryptor{
Variant: strings.Split(tt.cipher, "/"),
Key: config.NewSecret([]byte(tt.key)),
Vec: config.NewSecret([]byte(iv)),
}
require.NoError(t, decrypter.Init())
enc, err := hex.DecodeString(tt.encrypted)
require.NoError(t, err)
dec, err := decrypter.Decrypt(enc)
require.NoError(t, err)
require.Equal(t, expected, string(dec))
})
}
}
func TestAESNoPadding(t *testing.T) {
keySource := hex.EncodeToString([]byte("0123456789abcdefghijklmnopqrstuvwxyz"))
expected := "my $ecret-Passw0rd"
iv := hex.EncodeToString([]byte("0123456789abcdef"))
tests := []struct {
cipher string
encrypted string
key string
}{
{
cipher: "AES128/CBC",
encrypted: "9E36B490B0B1D6CE28550DF9DE65FC0013FF9F0939E24DA4A24324BDB5EABA04",
key: keySource[:32],
},
{
cipher: "AES192/CBC",
encrypted: "D3A5A0004B6783351F89B00C1D4154EDF2321EDAD3111B5551C18836B9FCFD62",
key: keySource[:48],
},
{
cipher: "AES256/CBC",
encrypted: "9751D7FB4B1497DEBC8A95C5D88097ECB1B8E63979E2D41E7ECD304D6B39B808",
key: keySource[:64],
},
}
for _, tt := range tests {
t.Run(tt.cipher, func(t *testing.T) {
decrypter := AesEncryptor{
Variant: strings.Split(tt.cipher, "/"),
Key: config.NewSecret([]byte(tt.key)),
Vec: config.NewSecret([]byte(iv)),
}
require.NoError(t, decrypter.Init())
enc, err := hex.DecodeString(tt.encrypted)
require.NoError(t, err)
dec, err := decrypter.Decrypt(enc)
require.NoError(t, err)
require.Len(t, string(dec), 32)
require.Contains(t, string(dec), expected)
})
}
}
func TestAESKDF(t *testing.T) {
expected := "my $ecret-Passw0rd"
iv := hex.EncodeToString([]byte("asupersecretiv42"))
tests := []struct {
cipher string
password string
salt string
iterations int
encrypted string
}{
{
cipher: "AES256/CBC/PKCS#5",
password: "a secret password",
salt: "somerandombytes",
iterations: 2000,
encrypted: "224b169206ce918f167ae0da18f4de45bede0d2c853d45e55f1422d1446037bf",
},
}
for _, tt := range tests {
t.Run(tt.cipher, func(t *testing.T) {
decrypter := AesEncryptor{
Variant: strings.Split(tt.cipher, "/"),
KDFConfig: KDFConfig{
Algorithm: "PBKDF2-HMAC-SHA256",
Passwd: config.NewSecret([]byte(tt.password)),
Salt: config.NewSecret([]byte(tt.salt)),
Iterations: tt.iterations,
},
Vec: config.NewSecret([]byte(iv)),
}
require.NoError(t, decrypter.Init())
enc, err := hex.DecodeString(tt.encrypted)
require.NoError(t, err)
dec, err := decrypter.Decrypt(enc)
require.NoError(t, err)
require.Equal(t, expected, string(dec))
})
}
}
func TestAESInitErrors(t *testing.T) {
tests := []struct {
name string
variant []string
key string
iv string
kdfcfg *KDFConfig
expected string
}{
{
name: "no mode",
variant: []string{"AES128"},
expected: "please specify cipher mode",
},
{
name: "too many elements",
variant: []string{"AES128", "CBC", "PKCS#5", "superfluous"},
expected: "too many variant elements",
},
{
name: "no AES",
variant: []string{"rsa", "cbc"},
expected: `requested AES but specified "rsa"`,
},
{
name: "no cipher",
expected: "please specify cipher",
},
{
name: "unsupported cipher",
variant: []string{"aes64", "cbc"},
expected: "unsupported AES cipher",
},
{
name: "unsupported mode",
variant: []string{"aes128", "foo"},
expected: "unsupported cipher mode",
},
{
name: "unsupported padding",
variant: []string{"aes128", "cbc", "bar"},
expected: "unsupported padding",
},
{
name: "missing key",
variant: []string{"aes128", "cbc", "none"},
expected: "either key or password has to be specified",
},
{
name: "wrong key length",
variant: []string{"aes256", "cbc"},
key: "63238c069e3c5d6aaa20048c43ce4ed0",
expected: "key length (128 bit) does not match cipher (256 bit)",
},
{
name: "invalid key",
variant: []string{"aes256", "cbc"},
key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
expected: "decoding key failed: encoding/hex: invalid byte: U+0078 'x'",
},
{
name: "missing IV",
variant: []string{"aes128", "cbc"},
key: "63238c069e3c5d6aaa20048c43ce4ed0",
expected: "'init_vector' has to be specified or derived from password",
},
{
name: "invalid IV",
variant: []string{"aes128", "cbc"},
key: "63238c069e3c5d6aaa20048c43ce4ed0",
iv: "abcd",
expected: "init vector size must match block size",
},
{
name: "missing salt and iterations",
variant: []string{"aes128", "cbc", "none"},
kdfcfg: &KDFConfig{
Passwd: config.NewSecret([]byte("secret")),
},
expected: "salt and iterations required for password-based-keys",
},
{
name: "wrong keygen algorithm",
variant: []string{"aes128", "cbc", "none"},
kdfcfg: &KDFConfig{
Algorithm: "foo",
Passwd: config.NewSecret([]byte("secret")),
Salt: config.NewSecret([]byte("salt")),
Iterations: 2000,
},
expected: "unknown key-derivation function",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.NotEmpty(t, tt.expected)
decrypter := AesEncryptor{
Variant: tt.variant,
}
if tt.key != "" {
decrypter.Key = config.NewSecret([]byte(tt.key))
}
if tt.iv != "" {
decrypter.Vec = config.NewSecret([]byte(tt.iv))
}
if tt.kdfcfg != nil {
decrypter.KDFConfig = *tt.kdfcfg
}
require.ErrorContains(t, decrypter.Init(), tt.expected)
})
}
}
func TestAESDecryptError(t *testing.T) {
tests := []struct {
name string
encrypted string
messMode string
messKey string
messIV string
expected string
}{
{
name: "wrong data length",
encrypted: "abcd",
expected: "invalid data size",
},
{
name: "mode tampered",
encrypted: "9E36B490B0B1D6CE28550DF9DE65FC0013FF9F0939E24DA4A24324BDB5EABA04",
messMode: "tampered",
expected: `unsupported cipher mode "tampered"`,
},
{
name: "invalid key",
encrypted: "9E36B490B0B1D6CE28550DF9DE65FC0013FF9F0939E24DA4A24324BDB5EABA04",
messKey: "tampered",
expected: "decoding key failed: encoding/hex: invalid byte: U+0074 't'",
},
{
name: "wrong key length",
encrypted: "9E36B490B0B1D6CE28550DF9DE65FC0013FF9F0939E24DA4A24324BDB5EABA04",
messKey: "01234567",
expected: "creating AES cipher failed: crypto/aes: invalid key size",
},
{
name: "invalid key",
encrypted: "9E36B490B0B1D6CE28550DF9DE65FC0013FF9F0939E24DA4A24324BDB5EABA04",
messIV: "tampered",
expected: "decoding init vector failed: encoding/hex: invalid byte: U+0074 't'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.NotEmpty(t, tt.expected)
decrypter := AesEncryptor{
Variant: []string{"AES128", "CBC", "PKCS#5"},
Key: config.NewSecret([]byte(hex.EncodeToString([]byte("0123456789abcdef")))),
Vec: config.NewSecret([]byte(hex.EncodeToString([]byte("0123456789abcdef")))),
}
require.NoError(t, decrypter.Init())
enc, err := hex.DecodeString(tt.encrypted)
require.NoError(t, err)
// Mess with the internal values for testing
if tt.messMode != "" {
decrypter.mode = tt.messMode
}
if tt.messKey != "" {
decrypter.Key = config.NewSecret([]byte(tt.messKey))
}
if tt.messIV != "" {
decrypter.Vec = config.NewSecret([]byte(tt.messIV))
}
_, err = decrypter.Decrypt(enc)
require.ErrorContains(t, err, tt.expected)
})
}
}

View file

@ -0,0 +1,47 @@
package http
import (
"errors"
"fmt"
"strings"
)
type Decrypter interface {
Decrypt(data []byte) ([]byte, error)
}
type DecryptionConfig struct {
Cipher string `toml:"cipher"`
Aes AesEncryptor `toml:"aes"`
}
func (c *DecryptionConfig) CreateDecrypter() (Decrypter, error) {
// For ciphers that allowing variants (e.g. AES256/CBC/PKCS#5Padding)
// can specify the variant using <algorithm>[/param 1>[/<param 2>]...]
// where all parameters will be passed on to the decrypter.
parts := strings.Split(c.Cipher, "/")
switch strings.ToLower(parts[0]) {
case "", "none":
return nil, nil
case "aes", "aes128", "aes192", "aes256":
c.Aes.Variant = parts
if err := c.Aes.Init(); err != nil {
return nil, fmt.Errorf("init of AES decrypter failed: %w", err)
}
return &c.Aes, nil
}
return nil, fmt.Errorf("unknown cipher %q", c.Cipher)
}
func PKCS5or7Trimming(in []byte) ([]byte, error) {
// 'count' number of bytes where padded to the end of the clear-text
// each containing the value of 'count'
if len(in) == 0 {
return nil, errors.New("empty value to trim")
}
count := int(in[len(in)-1])
if len(in) < count {
return nil, fmt.Errorf("length %d shorter than trim value %d", len(in), count)
}
return in[:len(in)-count], nil
}

View file

@ -0,0 +1,22 @@
package http
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestCreateAESFail(t *testing.T) {
cfg := DecryptionConfig{Cipher: "aes128/CBC/PKCS#5/garbage"}
decrypt, err := cfg.CreateDecrypter()
require.ErrorContains(t, err, "init of AES decrypter failed")
require.Nil(t, decrypt)
}
func TestTrimPKCSFail(t *testing.T) {
_, err := PKCS5or7Trimming(nil)
require.ErrorContains(t, err, "empty value to trim")
_, err = PKCS5or7Trimming([]byte{0x00, 0x05})
require.ErrorContains(t, err, "length 2 shorter than trim value 5")
}

View file

@ -0,0 +1,241 @@
//go:generate ../../../tools/readme_config_includer/generator
package http
import (
"context"
_ "embed"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/blues/jsonata-go"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
common_http "github.com/influxdata/telegraf/plugins/common/http"
"github.com/influxdata/telegraf/plugins/secretstores"
)
//go:embed sample.conf
var sampleConfig string
const defaultIdleConnTimeoutMinutes = 5
type HTTP struct {
URL string `toml:"url"`
Headers map[string]string `toml:"headers"`
Username config.Secret `toml:"username"`
Password config.Secret `toml:"password"`
Token config.Secret `toml:"token"`
SuccessStatusCodes []int `toml:"success_status_codes"`
Transformation string `toml:"transformation"`
Log telegraf.Logger `toml:"-"`
common_http.HTTPClientConfig
DecryptionConfig
client *http.Client
transformer *jsonata.Expr
cache map[string]string
decrypter Decrypter
}
func (*HTTP) SampleConfig() string {
return sampleConfig
}
func (h *HTTP) Init() error {
ctx := context.Background()
// Prevent idle connections from hanging around forever on telegraf reload
if h.HTTPClientConfig.IdleConnTimeout == 0 {
h.HTTPClientConfig.IdleConnTimeout = config.Duration(defaultIdleConnTimeoutMinutes * time.Minute)
}
client, err := h.HTTPClientConfig.CreateClient(ctx, h.Log)
if err != nil {
return err
}
h.client = client
// Set default as [200]
if len(h.SuccessStatusCodes) == 0 {
h.SuccessStatusCodes = []int{200}
}
// Setup the data transformer if any
if h.Transformation != "" {
e, err := jsonata.Compile(h.Transformation)
if err != nil {
return fmt.Errorf("setting up data transformation failed: %w", err)
}
h.transformer = e
}
// Setup the decryption infrastructure
h.decrypter, err = h.DecryptionConfig.CreateDecrypter()
if err != nil {
return fmt.Errorf("creating decryptor failed: %w", err)
}
return nil
}
// Get searches for the given key and return the secret
func (h *HTTP) Get(key string) ([]byte, error) {
v, found := h.cache[key]
if !found {
return nil, errors.New("not found")
}
if h.decrypter != nil {
// We got binary data delivered in a string, so try to
// decode it assuming base64-encoding.
buf, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return nil, fmt.Errorf("base64 decoding failed: %w", err)
}
return h.decrypter.Decrypt(buf)
}
return []byte(v), nil
}
// Set sets the given secret for the given key
func (*HTTP) Set(_, _ string) error {
return errors.New("setting secrets not supported")
}
// List lists all known secret keys
func (h *HTTP) List() ([]string, error) {
keys := make([]string, 0, len(h.cache))
for k := range h.cache {
keys = append(keys, k)
}
return keys, nil
}
// GetResolver returns a function to resolve the given key.
func (h *HTTP) GetResolver(key string) (telegraf.ResolveFunc, error) {
// Download and parse the credentials
if err := h.download(); err != nil {
return nil, err
}
resolver := func() ([]byte, bool, error) {
s, err := h.Get(key)
return s, false, err
}
return resolver, nil
}
func (h *HTTP) download() error {
// Get the raw data form the URL
data, err := h.query()
if err != nil {
return fmt.Errorf("reading body failed: %w", err)
}
// Transform the data to the expected form if given
if h.transformer != nil {
out, err := h.transformer.EvalBytes(data)
if err != nil {
return fmt.Errorf("transforming data failed: %w", err)
}
data = out
}
// Extract the data from the resulting data
if err := json.Unmarshal(data, &h.cache); err != nil {
var terr *json.UnmarshalTypeError
if errors.As(err, &terr) {
return fmt.Errorf("%w; maybe missing or wrong data transformation", err)
}
return err
}
return nil
}
func (h *HTTP) query() ([]byte, error) {
request, err := http.NewRequest(http.MethodGet, h.URL, nil)
if err != nil {
return nil, fmt.Errorf("creating request failed: %w", err)
}
for k, v := range h.Headers {
if strings.EqualFold(k, "host") {
request.Host = v
} else {
request.Header.Add(k, v)
}
}
if err := h.setRequestAuth(request); err != nil {
return nil, err
}
resp, err := h.client.Do(request)
if err != nil {
return nil, fmt.Errorf("executing request failed: %w", err)
}
defer resp.Body.Close()
// Try to wipe the bearer token if any
request.SetBasicAuth("---", "---")
request.Header.Set("Authorization", "---")
responseHasSuccessCode := false
for _, statusCode := range h.SuccessStatusCodes {
if resp.StatusCode == statusCode {
responseHasSuccessCode = true
break
}
}
if !responseHasSuccessCode {
msg := "received status code %d (%s), expected any value out of %v"
return nil, fmt.Errorf(msg, resp.StatusCode, http.StatusText(resp.StatusCode), h.SuccessStatusCodes)
}
return io.ReadAll(resp.Body)
}
func (h *HTTP) setRequestAuth(request *http.Request) error {
if !h.Username.Empty() && !h.Password.Empty() {
username, err := h.Username.Get()
if err != nil {
return fmt.Errorf("getting username failed: %w", err)
}
defer username.Destroy()
password, err := h.Password.Get()
if err != nil {
return fmt.Errorf("getting password failed: %w", err)
}
defer password.Destroy()
request.SetBasicAuth(username.String(), password.String())
}
if !h.Token.Empty() {
token, err := h.Token.Get()
if err != nil {
return fmt.Errorf("getting token failed: %w", err)
}
defer token.Destroy()
bearer := "Bearer " + strings.TrimSpace(token.String())
request.Header.Set("Authorization", bearer)
}
return nil
}
// Register the secret-store on load.
func init() {
secretstores.Add("http", func(string) telegraf.SecretStore {
return &HTTP{}
})
}

View file

@ -0,0 +1,433 @@
package http
import (
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/secretstores"
"github.com/influxdata/telegraf/testutil"
)
func TestCases(t *testing.T) {
// Get all directories in testcases
folders, err := os.ReadDir("testcases")
require.NoError(t, err)
// Make sure tests contains data
require.NotEmpty(t, folders)
// Set up for file inputs
secretstores.Add("http", func(string) telegraf.SecretStore {
return &HTTP{Log: testutil.Logger{}}
})
for _, f := range folders {
// Only handle folders
if !f.IsDir() {
continue
}
fname := f.Name()
t.Run(fname, func(t *testing.T) {
testdataPath := filepath.Join("testcases", fname)
configFilename := filepath.Join(testdataPath, "telegraf.conf")
inputFilename := filepath.Join(testdataPath, "secrets.json")
expectedFilename := filepath.Join(testdataPath, "expected.json")
// Read the input data
input, err := os.ReadFile(inputFilename)
require.NoError(t, err)
// Read the expected output data
buf, err := os.ReadFile(expectedFilename)
require.NoError(t, err)
var expected map[string]string
require.NoError(t, json.Unmarshal(buf, &expected))
// Configure the plugin
cfg := config.NewConfig()
require.NoError(t, cfg.LoadConfig(configFilename))
require.NotEmpty(t, cfg.SecretStores)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/secrets" {
if _, err = w.Write(input); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
us, err := url.Parse(server.URL)
require.NoError(t, err)
var id string
var plugin telegraf.SecretStore
actual := make(map[string]string, len(expected))
for id, plugin = range cfg.SecretStores {
// Setup dummy server and redirect the plugin's URL to that dummy
httpPlugin, ok := plugin.(*HTTP)
require.True(t, ok)
u, err := url.Parse(httpPlugin.URL)
require.NoError(t, err)
u.Host = us.Host
httpPlugin.URL = u.String()
require.NoError(t, httpPlugin.download())
// Retrieve the secrets from the plugin
keys, err := plugin.List()
require.NoError(t, err)
for _, k := range keys {
v, err := plugin.Get(k)
require.NoError(t, err)
actual[id+"."+k] = string(v)
}
}
require.EqualValues(t, expected, actual)
})
}
}
func TestSampleConfig(t *testing.T) {
plugin := &HTTP{}
require.NotEmpty(t, plugin.SampleConfig())
}
func TestInit(t *testing.T) {
plugin := &HTTP{
DecryptionConfig: DecryptionConfig{
Cipher: "AES128/CBC/PKCS#5",
Aes: AesEncryptor{
Key: config.NewSecret([]byte("7465737474657374657374746573740a")),
Vec: config.NewSecret([]byte("7465737474657374657374746573740a")),
},
},
}
require.NoError(t, plugin.Init())
}
func TestInitErrors(t *testing.T) {
plugin := &HTTP{Transformation: "{some: malformed"}
require.ErrorContains(t, plugin.Init(), "setting up data transformation failed")
plugin = &HTTP{DecryptionConfig: DecryptionConfig{Cipher: "non-existing/CBC/lala"}}
require.ErrorContains(t, plugin.Init(), "creating decryptor failed: unknown cipher")
}
func TestSetNotSupported(t *testing.T) {
plugin := &HTTP{}
require.NoError(t, plugin.Init())
require.ErrorContains(t, plugin.Set("key", "value"), "setting secrets not supported")
}
func TestGetErrors(t *testing.T) {
plugin := &HTTP{
DecryptionConfig: DecryptionConfig{
Cipher: "AES256/CBC/PKCS#5",
Aes: AesEncryptor{
Key: config.NewSecret([]byte("63238c069e3c5d6aaa20048c43ce4ed0a910eef95f22f55bacdddacafa06b656")),
Vec: config.NewSecret([]byte("61737570657273656372657469763432")),
},
},
}
require.NoError(t, plugin.Init())
_, err := plugin.Get("OMG")
require.ErrorContains(t, err, "not found")
plugin.cache = map[string]string{"test": "aedMZXaLR246OHHjVtJKXQ=X"}
_, err = plugin.Get("test")
require.ErrorContains(t, err, "base64 decoding failed")
}
func TestResolver(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if _, err := w.Write([]byte(`{"test": "aedMZXaLR246OHHjVtJKXQ=="}`)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
}))
defer server.Close()
plugin := &HTTP{
URL: server.URL,
DecryptionConfig: DecryptionConfig{
Cipher: "AES256/CBC/PKCS#5",
Aes: AesEncryptor{
Key: config.NewSecret([]byte("63238c069e3c5d6aaa20048c43ce4ed0a910eef95f22f55bacdddacafa06b656")),
Vec: config.NewSecret([]byte("61737570657273656372657469763432")),
},
},
}
plugin.Timeout = config.Duration(200 * time.Millisecond)
require.NoError(t, plugin.Init())
resolver, err := plugin.GetResolver("test")
require.NoError(t, err)
s, _, err := resolver()
require.NoError(t, err)
require.Equal(t, "password-B", string(s))
}
func TestGetResolverErrors(t *testing.T) {
dummy, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer dummy.Close()
plugin := &HTTP{
URL: "http://" + dummy.Addr().String(),
}
plugin.Timeout = config.Duration(200 * time.Millisecond)
require.NoError(t, plugin.Init())
_, err = plugin.GetResolver("test")
require.ErrorContains(t, err, "context deadline exceeded")
dummy.Close()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if _, err = w.Write([]byte(`[{"test": "aedMZXaLR246OHHjVtJKXQ=="}]`)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
}))
defer server.Close()
plugin = &HTTP{
URL: server.URL,
DecryptionConfig: DecryptionConfig{
Cipher: "AES256/CBC/PKCS#5",
Aes: AesEncryptor{
Key: config.NewSecret([]byte("63238c069e3c5d6aaa20048c43ce4ed0a910eef95f22f55bacdddacafa06b656")),
Vec: config.NewSecret([]byte("61737570657273656372657469763432")),
},
},
}
plugin.Timeout = config.Duration(200 * time.Millisecond)
require.NoError(t, plugin.Init())
_, err = plugin.GetResolver("test")
require.ErrorContains(t, err, "maybe missing or wrong data transformation")
plugin.Transformation = "{awe:skds}"
require.NoError(t, plugin.Init())
_, err = plugin.GetResolver("test")
require.ErrorContains(t, err, "transforming data failed")
}
func TestInvalidServerResponse(t *testing.T) {
dummy, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer dummy.Close()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if _, err = w.Write([]byte(`[somerandomebytes`)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
}))
defer server.Close()
plugin := &HTTP{
URL: server.URL,
DecryptionConfig: DecryptionConfig{
Cipher: "AES256/CBC/PKCS#5",
Aes: AesEncryptor{
Key: config.NewSecret([]byte("63238c069e3c5d6aaa20048c43ce4ed0a910eef95f22f55bacdddacafa06b656")),
Vec: config.NewSecret([]byte("61737570657273656372657469763432")),
},
},
}
plugin.Timeout = config.Duration(200 * time.Millisecond)
require.NoError(t, plugin.Init())
_, err = plugin.GetResolver("test")
require.Error(t, err)
var expectedErr *json.SyntaxError
require.ErrorAs(t, err, &expectedErr)
}
func TestAdditionalHeaders(t *testing.T) {
dummy, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer dummy.Close()
var actual http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
actual = r.Header.Clone()
if r.Host != "" {
actual.Add("host", r.Host)
}
if _, err = w.Write([]byte(`{"test": "aedMZXaLR246OHHjVtJKXQ=="}`)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
}))
defer server.Close()
plugin := &HTTP{
URL: server.URL,
Headers: map[string]string{
"host": "a.host.com",
"foo": "bar",
},
DecryptionConfig: DecryptionConfig{
Cipher: "AES256/CBC/PKCS#5",
Aes: AesEncryptor{
Key: config.NewSecret([]byte("63238c069e3c5d6aaa20048c43ce4ed0a910eef95f22f55bacdddacafa06b656")),
Vec: config.NewSecret([]byte("61737570657273656372657469763432")),
},
},
}
plugin.Timeout = config.Duration(200 * time.Millisecond)
require.NoError(t, plugin.Init())
require.NoError(t, plugin.download())
secret, err := plugin.Get("test")
require.NoError(t, err)
require.Equal(t, "password-B", string(secret))
for k, v := range plugin.Headers {
av := actual.Get(k)
require.NotEmptyf(t, av, "header %q not found", k)
require.Equal(t, v, av, "mismatch for header %q", k)
}
}
func TestServerReturnCodes(t *testing.T) {
dummy, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer dummy.Close()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/", "/200":
if _, err = w.Write([]byte(`{}`)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
case "/201":
w.WriteHeader(201)
case "/300":
w.WriteHeader(300)
if _, err = w.Write([]byte(`{}`)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
case "/401":
w.WriteHeader(401)
default:
w.WriteHeader(404)
}
}))
defer server.Close()
plugin := &HTTP{
URL: server.URL,
SuccessStatusCodes: []int{200, 300},
}
plugin.Timeout = config.Duration(200 * time.Millisecond)
require.NoError(t, plugin.Init())
// 200 and 300 should not return an error
require.NoError(t, plugin.download())
plugin.URL = server.URL + "/200"
require.NoError(t, plugin.download())
plugin.URL = server.URL + "/300"
require.NoError(t, plugin.download())
// other error codes should cause errors
plugin.URL = server.URL + "/201"
require.ErrorContains(t, plugin.download(), "received status code 201")
plugin.URL = server.URL + "/401"
require.ErrorContains(t, plugin.download(), "received status code 401")
plugin.URL = server.URL + "/somewhere"
require.ErrorContains(t, plugin.download(), "received status code 404")
}
func TestAuthenticationBasic(t *testing.T) {
dummy, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer dummy.Close()
var header http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header = r.Header
if _, err = w.Write([]byte(`{}`)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
}))
defer server.Close()
plugin := &HTTP{
URL: server.URL,
Username: config.NewSecret([]byte("myuser")),
Password: config.NewSecret([]byte("mypass")),
SuccessStatusCodes: []int{200, 300},
}
plugin.Timeout = config.Duration(200 * time.Millisecond)
require.NoError(t, plugin.Init())
require.NoError(t, plugin.download())
auth := header.Get("Authorization")
require.NotEmpty(t, auth)
require.Equal(t, "Basic bXl1c2VyOm15cGFzcw==", auth)
}
func TestAuthenticationToken(t *testing.T) {
dummy, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer dummy.Close()
var header http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header = r.Header
if _, err = w.Write([]byte(`{}`)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
t.Error(err)
return
}
}))
defer server.Close()
token := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJUaWdlciIsImlhdCI6M..."
plugin := &HTTP{
URL: server.URL,
Token: config.NewSecret([]byte(token)),
SuccessStatusCodes: []int{200, 300},
}
plugin.Timeout = config.Duration(200 * time.Millisecond)
require.NoError(t, plugin.Init())
require.NoError(t, plugin.download())
auth := header.Get("Authorization")
require.NotEmpty(t, auth)
require.Equal(t, "Bearer "+token, auth)
}

View file

@ -0,0 +1,53 @@
package http
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"hash"
"strings"
"golang.org/x/crypto/pbkdf2"
"github.com/influxdata/telegraf/config"
)
type KDFConfig struct {
Algorithm string `toml:"kdf_algorithm"`
Passwd config.Secret `toml:"password"`
Salt config.Secret `toml:"salt"`
Iterations int `toml:"iterations"`
}
type hashFunc func() hash.Hash
func (k *KDFConfig) NewKey(keylen int) (key, iv config.Secret, err error) {
switch strings.ToUpper(k.Algorithm) {
case "", "PBKDF2-HMAC-SHA256":
return k.generatePBKDF2HMAC(sha256.New, keylen)
}
return config.Secret{}, config.Secret{}, fmt.Errorf("unknown key-derivation function %q", k.Algorithm)
}
func (k *KDFConfig) generatePBKDF2HMAC(hf hashFunc, keylen int) (key, iv config.Secret, err error) {
if k.Iterations == 0 {
return config.Secret{}, config.Secret{}, errors.New("'iteration value not set")
}
passwd, err := k.Passwd.Get()
if err != nil {
return config.Secret{}, config.Secret{}, fmt.Errorf("getting password failed: %w", err)
}
defer passwd.Destroy()
salt, err := k.Salt.Get()
if err != nil {
return config.Secret{}, config.Secret{}, fmt.Errorf("getting salt failed: %w", err)
}
defer salt.Destroy()
rawkey := pbkdf2.Key(passwd.Bytes(), salt.Bytes(), k.Iterations, keylen, hf)
key = config.NewSecret([]byte(hex.EncodeToString(rawkey)))
return key, config.Secret{}, nil
}

View file

@ -0,0 +1,91 @@
package http
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/config"
)
func TestKDF(t *testing.T) {
tests := []struct {
algorithm string
password string
salt string
iterations int
length int
key string
iv string
}{
{
algorithm: "PBKDF2-HMAC-SHA256",
password: "a secret password",
salt: "somerandombytes",
iterations: 2000,
length: 16,
key: "f49817e5faa63d9bb631b143c7d11ff7",
},
}
for _, tt := range tests {
t.Run(tt.algorithm, func(t *testing.T) {
cfg := KDFConfig{
Algorithm: tt.algorithm,
Passwd: config.NewSecret([]byte(tt.password)),
Salt: config.NewSecret([]byte(tt.salt)),
Iterations: tt.iterations,
}
skey, siv, err := cfg.NewKey(16)
require.NoError(t, err)
require.NotNil(t, skey)
require.NotNil(t, siv)
key, err := skey.Get()
require.NoError(t, err)
defer key.Destroy()
require.Equal(t, tt.key, key.TemporaryString())
if tt.iv != "" {
iv, err := siv.Get()
require.NoError(t, err)
defer iv.Destroy()
require.Equal(t, tt.iv, iv.TemporaryString())
} else {
require.True(t, siv.Empty())
}
})
}
}
func TestKDFErrors(t *testing.T) {
tests := []struct {
name string
password string
salt string
iterations int
length int
expected string
}{
{
name: "missing iterations",
password: "a secret password",
salt: "somerandombytes",
length: 16,
expected: "iteration value not set",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.NotEmpty(t, tt.expected)
cfg := KDFConfig{
Algorithm: "PBKDF2-HMAC-SHA256",
Passwd: config.NewSecret([]byte(tt.password)),
Salt: config.NewSecret([]byte(tt.salt)),
Iterations: tt.iterations,
}
_, _, err := cfg.NewKey(16)
require.ErrorContains(t, err, tt.expected)
})
}
}

View file

@ -0,0 +1,85 @@
# Read secrets from a HTTP endpoint
[[secretstores.http]]
## Unique identifier for the secret-store.
## This id can later be used in plugins to reference the secrets
## in this secret-store via @{<id>:<secret_key>} (mandatory)
id = "secretstore"
## URLs from which to read the secrets
url = "http://localhost/secrets"
## Optional HTTP headers
# headers = {"X-Special-Header" = "Special-Value"}
## Optional Token for Bearer Authentication via
## "Authorization: Bearer <token>" header
# token = "your-token"
## Optional Credentials for HTTP Basic Authentication
# username = "username"
# password = "pa$$word"
## OAuth2 Client Credentials. The options 'client_id', 'client_secret', and 'token_url' are required to use OAuth2.
# client_id = "clientid"
# client_secret = "secret"
# token_url = "https://indentityprovider/oauth2/v1/token"
# scopes = ["urn:opc:idm:__myscopes__"]
## HTTP Proxy support
# use_system_proxy = false
# http_proxy_url = ""
## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
## Minimal TLS version to accept by the client
# tls_min_version = "TLS12"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false
## Optional Cookie authentication
# cookie_auth_url = "https://localhost/authMe"
# cookie_auth_method = "POST"
# cookie_auth_username = "username"
# cookie_auth_password = "pa$$word"
# cookie_auth_headers = { Content-Type = "application/json", X-MY-HEADER = "hello" }
# cookie_auth_body = '{"username": "user", "password": "pa$$word", "authenticate": "me"}'
## When unset or set to zero the authentication will only happen once
## and will never renew the cookie. Set to a suitable duration if you
## require cookie renewal!
# cookie_auth_renewal = "0s"
## Amount of time allowed to complete the HTTP request
# timeout = "5s"
## List of success status codes
# success_status_codes = [200]
## JSONata expression to transform the server response into a
## { "secret name": "secret value", ... }
## form. See https://jsonata.org for more information and a playground.
# transformation = ''
## Cipher used to decrypt the secrets.
## In case your secrets are transmitted in an encrypted form, you need
## to specify the cipher used and provide the corresponding configuration.
## Please refer to https://github.com/influxdata/telegraf/blob/master/plugins/secretstores/http/README.md
## for supported values.
# cipher = "none"
## AES cipher parameters
# [secretstores.http.aes]
# ## Key (hex-encoded) and initialization-vector (IV) for the decryption.
# ## In case the key (and IV) is derived from a password, the values can
# ## be omitted.
# key = ""
# init_vector = ""
#
# ## Parameters for password-based-key derivation.
# ## These parameters must match the encryption side to derive the same
# ## key on both sides!
# # kdf_algorithm = "PBKDF2-HMAC-SHA256"
# # password = ""
# # salt = ""
# # iterations = 0

View file

@ -0,0 +1,6 @@
{
"test.user_1": "password A",
"test.user 2": "password-B",
"test.user@company.com": "my$3cR3T",
"test.user %with% $trAng€ characters": ""
}

View file

@ -0,0 +1,6 @@
{
"user_1": "3y1Za18sLLNIIHw1fv2Olg==",
"user 2": "aedMZXaLR246OHHjVtJKXQ==",
"user@company.com": "rcFobNmuaaboSPZY5nKjzQ==",
"user %with% $trAng\u20ac characters": "1HxPInsJomaWAE19VBisyw=="
}

View file

@ -0,0 +1,12 @@
[[secretstores.http]]
id = "test"
url = "http://127.0.0.1/secrets"
cipher = "AES256/CBC/PKCS#5"
[secretstores.http.aes]
init_vector = "61737570657273656372657469763432"
kdf_algorithm = "PBKDF2-HMAC-SHA256"
password = "a secret key"
salt = "somerandombytes"
iterations = 2000

View file

@ -0,0 +1,6 @@
{
"test.user_1": "password A",
"test.user 2": "password-B",
"test.user@company.com": "my$3cR3T",
"test.user %with% $trAng€ characters": ""
}

View file

@ -0,0 +1,6 @@
{
"user_1": "3y1Za18sLLNIIHw1fv2Olg==",
"user 2": "aedMZXaLR246OHHjVtJKXQ==",
"user@company.com": "rcFobNmuaaboSPZY5nKjzQ==",
"user %with% $trAng\u20ac characters": "1HxPInsJomaWAE19VBisyw=="
}

View file

@ -0,0 +1,9 @@
[[secretstores.http]]
id = "test"
url = "http://127.0.0.1/secrets"
cipher = "AES256/CBC/PKCS#5"
[secretstores.http.aes]
key = "63238c069e3c5d6aaa20048c43ce4ed0a910eef95f22f55bacdddacafa06b656"
init_vector = "61737570657273656372657469763432"

View file

@ -0,0 +1,10 @@
{
"user.user_a": "user_1",
"passwd.password_a": "password A",
"user.user_b": "user 2",
"passwd.password_b": "password-B",
"user.user_c": "user@company.com",
"passwd.password_c": "my$3cR3T",
"user.user_d": "user %with% $trAng€ characters",
"passwd.password_d": ""
}

View file

@ -0,0 +1,30 @@
[
{
"userName": "user_a",
"userValue": "user_1",
"secretName": "password_a",
"secretValue": "3y1Za18sLLNIIHw1fv2Olg==",
"description": "server credentials for A"
},
{
"userName": "user_b",
"userValue": "user 2",
"secretName": "password_b",
"secretValue": "aedMZXaLR246OHHjVtJKXQ==",
"description": "server credentials for B"
},
{
"userName": "user_c",
"userValue": "user@company.com",
"secretName": "password_c",
"secretValue": "rcFobNmuaaboSPZY5nKjzQ==",
"description": "server credentials for C"
},
{
"userName": "user_d",
"userValue": "user %with% $trAng€ characters",
"secretName": "password_d",
"secretValue": "1HxPInsJomaWAE19VBisyw==",
"description": "server credentials for D"
}
]

View file

@ -0,0 +1,15 @@
[[secretstores.http]]
id = "user"
url = "http://127.0.0.1/secrets"
transformation = '{userName: userValue}'
[[secretstores.http]]
id = "passwd"
url = "http://127.0.0.1/secrets"
transformation = '{secretName: secretValue}'
cipher = "AES256/CBC/PKCS#5"
[secretstores.http.aes]
key = "63238c069e3c5d6aaa20048c43ce4ed0a910eef95f22f55bacdddacafa06b656"
init_vector = "61737570657273656372657469763432"

View file

@ -0,0 +1,10 @@
{
"test.user_a": "user_1",
"test.password_a": "password A",
"test.user_b": "user 2",
"test.password_b": "password-B",
"test.user_c": "user@company.com",
"test.password_c": "my$3cR3T",
"test.user_d": "user %with% $trAng€ characters",
"test.password_d": ""
}

View file

@ -0,0 +1,30 @@
[
{
"userName": "user_a",
"userValue": "user_1",
"secretName": "password_a",
"secretValue": "password A",
"description": "server credentials for A"
},
{
"userName": "user_b",
"userValue": "user 2",
"secretName": "password_b",
"secretValue": "password-B",
"description": "server credentials for B"
},
{
"userName": "user_c",
"userValue": "user@company.com",
"secretName": "password_c",
"secretValue": "my$3cR3T",
"description": "server credentials for C"
},
{
"userName": "user_d",
"userValue": "user %with% $trAng€ characters",
"secretName": "password_d",
"secretValue": "",
"description": "server credentials for D"
}
]

View file

@ -0,0 +1,4 @@
[[secretstores.http]]
id = "test"
url = "http://127.0.0.1/secrets"
transformation = '{userName: userValue, secretName: secretValue}'

View file

@ -0,0 +1,6 @@
{
"test.user_1": "password A",
"test.user 2": "password-B",
"test.user@company.com": "my$3cR3T",
"test.user %with% $trAng€ characters": ""
}

View file

@ -0,0 +1,18 @@
[
{
"user": "user_1",
"secret": "password A"
},
{
"user": "user 2",
"secret": "password-B"
},
{
"user": "user@company.com",
"secret": "my$3cR3T"
},
{
"user": "user %with% $trAng€ characters",
"secret": ""
}
]

View file

@ -0,0 +1,4 @@
[[secretstores.http]]
id = "test"
url = "http://127.0.0.1/secrets"
transformation = '{user: secret}'

View file

@ -0,0 +1,6 @@
{
"test.user_1": "password A",
"test.user 2": "password-B",
"test.user@company.com": "my$3cR3T",
"test.user %with% $trAng€ characters": ""
}

View file

@ -0,0 +1,6 @@
{
"user_1": "password A",
"user 2": "password-B",
"user@company.com": "my$3cR3T",
"user %with% $trAng€ characters": ""
}

View file

@ -0,0 +1,3 @@
[[secretstores.http]]
id = "test"
url = "http://127.0.0.1/secrets"