Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
175
plugins/secretstores/http/README.md
Normal file
175
plugins/secretstores/http/README.md
Normal 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!
|
172
plugins/secretstores/http/aes.go
Normal file
172
plugins/secretstores/http/aes.go
Normal 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)
|
||||
}
|
319
plugins/secretstores/http/aes_test.go
Normal file
319
plugins/secretstores/http/aes_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
47
plugins/secretstores/http/decryption.go
Normal file
47
plugins/secretstores/http/decryption.go
Normal 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
|
||||
}
|
22
plugins/secretstores/http/decryption_test.go
Normal file
22
plugins/secretstores/http/decryption_test.go
Normal 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")
|
||||
}
|
241
plugins/secretstores/http/http.go
Normal file
241
plugins/secretstores/http/http.go
Normal 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{}
|
||||
})
|
||||
}
|
433
plugins/secretstores/http/http_test.go
Normal file
433
plugins/secretstores/http/http_test.go
Normal 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)
|
||||
}
|
53
plugins/secretstores/http/key_derivation.go
Normal file
53
plugins/secretstores/http/key_derivation.go
Normal 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
|
||||
}
|
91
plugins/secretstores/http/key_derivation_test.go
Normal file
91
plugins/secretstores/http/key_derivation_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
85
plugins/secretstores/http/sample.conf
Normal file
85
plugins/secretstores/http/sample.conf
Normal 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
|
|
@ -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": ""
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"user_1": "3y1Za18sLLNIIHw1fv2Olg==",
|
||||
"user 2": "aedMZXaLR246OHHjVtJKXQ==",
|
||||
"user@company.com": "rcFobNmuaaboSPZY5nKjzQ==",
|
||||
"user %with% $trAng\u20ac characters": "1HxPInsJomaWAE19VBisyw=="
|
||||
}
|
|
@ -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
|
|
@ -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": ""
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"user_1": "3y1Za18sLLNIIHw1fv2Olg==",
|
||||
"user 2": "aedMZXaLR246OHHjVtJKXQ==",
|
||||
"user@company.com": "rcFobNmuaaboSPZY5nKjzQ==",
|
||||
"user %with% $trAng\u20ac characters": "1HxPInsJomaWAE19VBisyw=="
|
||||
}
|
|
@ -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"
|
10
plugins/secretstores/http/testcases/mixed/expected.json
Normal file
10
plugins/secretstores/http/testcases/mixed/expected.json
Normal 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": ""
|
||||
}
|
30
plugins/secretstores/http/testcases/mixed/secrets.json
Normal file
30
plugins/secretstores/http/testcases/mixed/secrets.json
Normal 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"
|
||||
}
|
||||
]
|
15
plugins/secretstores/http/testcases/mixed/telegraf.conf
Normal file
15
plugins/secretstores/http/testcases/mixed/telegraf.conf
Normal 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"
|
|
@ -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": ""
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,4 @@
|
|||
[[secretstores.http]]
|
||||
id = "test"
|
||||
url = "http://127.0.0.1/secrets"
|
||||
transformation = '{userName: userValue, secretName: secretValue}'
|
|
@ -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": ""
|
||||
}
|
|
@ -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": ""
|
||||
}
|
||||
]
|
|
@ -0,0 +1,4 @@
|
|||
[[secretstores.http]]
|
||||
id = "test"
|
||||
url = "http://127.0.0.1/secrets"
|
||||
transformation = '{user: secret}'
|
|
@ -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": ""
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"user_1": "password A",
|
||||
"user 2": "password-B",
|
||||
"user@company.com": "my$3cR3T",
|
||||
"user %with% $trAng€ characters": ""
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
[[secretstores.http]]
|
||||
id = "test"
|
||||
url = "http://127.0.0.1/secrets"
|
Loading…
Add table
Add a link
Reference in a new issue