diff --git a/docs/LICENSE_OF_DEPENDENCIES.md b/docs/LICENSE_OF_DEPENDENCIES.md index e4c6247d2e1d7..b4f6ff543001b 100644 --- a/docs/LICENSE_OF_DEPENDENCIES.md +++ b/docs/LICENSE_OF_DEPENDENCIES.md @@ -71,6 +71,7 @@ following works: - github.com/awnumar/memcall [Apache License 2.0](https://github.com/awnumar/memcall/blob/master/LICENSE) - github.com/awnumar/memguard [Apache License 2.0](https://github.com/awnumar/memguard/blob/master/LICENSE) - github.com/aws/aws-msk-iam-sasl-signer-go [Apache License 2.0](https://github.com/aws/aws-msk-iam-sasl-signer-go/blob/main/LICENSE) +- github.com/aws/aws-sdk-go [Apache License 2.0](https://github.com/aws/aws-sdk-go/blob/main/LICENSE.txt) - github.com/aws/aws-sdk-go-v2 [Apache License 2.0](https://github.com/aws/aws-sdk-go-v2/blob/main/LICENSE.txt) - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream [Apache License 2.0](https://github.com/aws/aws-sdk-go-v2/blob/main/aws/protocol/eventstream/LICENSE.txt) - github.com/aws/aws-sdk-go-v2/config [Apache License 2.0](https://github.com/aws/aws-sdk-go-v2/blob/main/config/LICENSE.txt) @@ -231,6 +232,7 @@ following works: - github.com/hashicorp/go-multierror [Mozilla Public License 2.0](https://github.com/hashicorp/go-multierror/blob/master/LICENSE) - github.com/hashicorp/go-retryablehttp [Mozilla Public License 2.0](https://github.com/hashicorp/go-retryablehttp/blob/main/LICENSE) - github.com/hashicorp/go-rootcerts [Mozilla Public License 2.0](https://github.com/hashicorp/go-rootcerts/blob/master/LICENSE) +- github.com/hashicorp/go-secure-stdlib/awsutil [Mozilla Public License 2.0](https://github.com/hashicorp/go-secure-stdlib/blob/main/awsutil/LICENSE) - github.com/hashicorp/go-secure-stdlib/parseutil [Mozilla Public License 2.0](https://github.com/hashicorp/go-secure-stdlib/blob/main/parseutil/LICENSE) - github.com/hashicorp/go-secure-stdlib/strutil [Mozilla Public License 2.0](https://github.com/hashicorp/go-secure-stdlib/blob/main/strutil/LICENSE) - github.com/hashicorp/go-sockaddr [Mozilla Public License 2.0](https://github.com/hashicorp/go-sockaddr/blob/master/LICENSE) @@ -242,6 +244,10 @@ following works: - github.com/hashicorp/serf [Mozilla Public License 2.0](https://github.com/hashicorp/serf/blob/master/LICENSE) - github.com/hashicorp/vault/api [Mozilla Public License 2.0](https://github.com/hashicorp/vault/blob/main/api/LICENSE) - github.com/hashicorp/vault/api/auth/approle [Mozilla Public License 2.0](https://github.com/hashicorp/vault/blob/main/api/auth/approle/LICENSE) +- github.com/hashicorp/vault/api/auth/aws [Mozilla Public License 2.0](https://github.com/hashicorp/vault/blob/main/api/auth/aws/LICENSE) +- github.com/hashicorp/vault/api/auth/azure [Mozilla Public License 2.0](https://github.com/hashicorp/vault/blob/main/api/auth/azure/LICENSE) +- github.com/hashicorp/vault/api/auth/kubernetes [Mozilla Public License 2.0](https://github.com/hashicorp/vault/blob/main/api/auth/kubernetes/LICENSE) +- github.com/hashicorp/vault/api/auth/userpass [Mozilla Public License 2.0](https://github.com/hashicorp/vault/blob/main/api/auth/userpass/LICENSE) - github.com/huandu/xstrings [MIT License](https://github.com/huandu/xstrings/blob/master/LICENSE) - github.com/icholy/digest [MIT License](https://github.com/icholy/digest/blob/master/LICENSE) - github.com/imdario/mergo [BSD 3-Clause "New" or "Revised" License](https://github.com/imdario/mergo/blob/master/LICENSE) diff --git a/go.mod b/go.mod index 3b1475c536921..9460bf64b3737 100644 --- a/go.mod +++ b/go.mod @@ -124,6 +124,10 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/vault/api v1.22.0 github.com/hashicorp/vault/api/auth/approle v0.11.0 + github.com/hashicorp/vault/api/auth/aws v0.11.0 + github.com/hashicorp/vault/api/auth/azure v0.10.0 + github.com/hashicorp/vault/api/auth/kubernetes v0.10.0 + github.com/hashicorp/vault/api/auth/userpass v0.11.0 github.com/influxdata/influxdb-observability/common v0.5.12 github.com/influxdata/influxdb-observability/influx2otel v0.5.12 github.com/influxdata/influxdb-observability/otel2influx v0.5.12 @@ -308,6 +312,7 @@ require ( github.com/aristanetworks/glog v0.0.0-20191112221043-67e8567f59f3 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/awnumar/memcall v0.4.0 // indirect + github.com/aws/aws-sdk-go v1.55.7 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.43 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect @@ -415,6 +420,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/awsutil v0.3.0 // indirect github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.7 // indirect diff --git a/go.sum b/go.sum index d9d350c2f92ee..2ae85e51cd5b1 100644 --- a/go.sum +++ b/go.sum @@ -892,7 +892,10 @@ github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4 h1:2jAwFwA0Xgcx94dUId+K24yFabsK github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4/go.mod h1:MVYeeOhILFFemC/XlYTClvBjYZrg/EPd3ts885KrNTI= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.29.11/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= +github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.44.263/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= +github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= @@ -1579,6 +1582,7 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -1597,6 +1601,8 @@ github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/awsutil v0.3.0 h1:I8bynUKMh9I7JdwtW9voJ0xmHvBpxQtLjrMFDYmhOxY= +github.com/hashicorp/go-secure-stdlib/awsutil v0.3.0/go.mod h1:oKHSQs4ivIfZ3fbXGQOop1XuDfdSb8RIsWTGaAanSfg= github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= @@ -1639,6 +1645,14 @@ github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicH github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= github.com/hashicorp/vault/api/auth/approle v0.11.0 h1:ViUvgqoSTqHkMi1L1Rr/LnQ+PWiRaGUBGvx4UPfmKOw= github.com/hashicorp/vault/api/auth/approle v0.11.0/go.mod h1:v8ZqBRw+GP264ikIw2sEBKF0VT72MEhLWnZqWt3xEG8= +github.com/hashicorp/vault/api/auth/aws v0.11.0 h1:lWdUxrzvPotg6idNr62al4w97BgI9xTDdzMCTViNH2s= +github.com/hashicorp/vault/api/auth/aws v0.11.0/go.mod h1:PWqdH/xqaudapmnnGP9ip2xbxT/kRW2qEgpqiQff6Gc= +github.com/hashicorp/vault/api/auth/azure v0.10.0 h1:soTc1xmzmszDN3+xtKn1MpaWE1/mRPVC418J9Z1uP5I= +github.com/hashicorp/vault/api/auth/azure v0.10.0/go.mod h1:5u/66YseDanWOycDJhEu6frHmsMw4UFnHK0I7w3AVx8= +github.com/hashicorp/vault/api/auth/kubernetes v0.10.0 h1:5rqWmUFxnu3S7XYq9dafURwBgabYDFzo2Wv+AMopPHs= +github.com/hashicorp/vault/api/auth/kubernetes v0.10.0/go.mod h1:cZZmhF6xboMDmDbMY52oj2DKW6gS0cQ9g0pJ5XIXQ5U= +github.com/hashicorp/vault/api/auth/userpass v0.11.0 h1:iPw1PL6vzQTn2w14quKd0ZnJV+cfPe+p5CA22M45jsA= +github.com/hashicorp/vault/api/auth/userpass v0.11.0/go.mod h1:FZ/baZ5rhruevb6kED9eh9KhorGtwM+xxVBvtXSxZsY= github.com/henrybear327/Proton-API-Bridge v1.0.0 h1:gjKAaWfKu++77WsZTHg6FUyPC5W0LTKWQciUm8PMZb0= github.com/henrybear327/Proton-API-Bridge v1.0.0/go.mod h1:gunH16hf6U74W2b9CGDaWRadiLICsoJ6KRkSt53zLts= github.com/henrybear327/go-proton-api v1.0.0 h1:zYi/IbjLwFAW7ltCeqXneUGJey0TN//Xo851a/BgLXw= @@ -1752,6 +1766,7 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS github.com/jlaffaye/ftp v0.2.1-0.20240918233326-1b970516f5d3 h1:ZxO6Qr2GOXPdcW80Mcn3nemvilMPvpWqxrNfK2ZnNNs= github.com/jlaffaye/ftp v0.2.1-0.20240918233326-1b970516f5d3/go.mod h1:dvLUr/8Fs9a2OBrEnCC5duphbkz/k/mSy5OkXg3PAgI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= diff --git a/plugins/secretstores/vault/README.md b/plugins/secretstores/vault/README.md index d7b48ddab85b7..3d05dc228d188 100644 --- a/plugins/secretstores/vault/README.md +++ b/plugins/secretstores/vault/README.md @@ -2,7 +2,7 @@ The `vault` plugin allows to utilize secrets stored in a [HashiCorp Vault][vault] server via the Vault API. It supports authentication -via AppRole. +via AppRole, Userpass, AWS IAM, AWS EC2, Azure and Kubernetes. ⭐ Telegraf v1.37.0 🏷️ secrets @@ -49,15 +49,67 @@ store usage. ## By default will use the kv-v2 engine. # engine = "kv-v2" - [secretstores.vault.approle] - ## The Role ID for AppRole Authentication, a UUID string - role_id = "" + # [secretstores.vault.approle] + # ## The Role ID for AppRole Authentication, a UUID string + # role_id = "" + # + # ## Whether the Secret ID is configured to be response wrapped or not + # # response_wrapped = false + # + # ## The Secret ID for AppRole Authentication + # secret = "" - ## Whether the Secret ID is configured to be response wrapped or not - # response_wrapped = false + # [secretstores.vault.aws_ec2] + # ## The Role Name for AWS EC2 authentication + # role_name = "" + # + # ## The AWS region, defaulting to "us-east-1" if unset + # # region = "us-east-1" + # + # ## The signature type to use, defaulting to "pkcs7" + # ## Allowed options: "pkcs7", "identity", "rsa2048" + # # signature_type = "pkcs7" + + # ## Credentials will be set using the values in the environment variables: + # ## - AWS_ACCESS_KEY_ID + # ## - AWS_SECRET_ACCESS_KEY + # ## - AWS_SESSION_TOKEN + # ## To specify a path to a credentials file instead, set: + # ## - AWS_SHARED_CREDENTIALS_FILE + # [secretstores.vault.aws_iam] + # ## The Role Name for AWS IAM authentication + # role_name = "" + # + # ## The AWS region, defaulting to "us-east-1" if unset + # # region = "us-east-1" + # + # ## An optional server ID header to provide, with the key + # ## "X-Vault-AWS-IAM-Server-ID" + # # server_id_header = "" + + # [secretstores.vault.azure] + # ## The Role Name for Azure authentication + # role_name = "" + # + # ## The Azure Resource URL to use as the aud value on the JWT token to + # ## use rather than the default of Azure Public Cloud's ARM URL. + # ## Defaults to "https://management.azure.com/" + # # resource_url = "https://management.azure.com/" + + # [secretstores.vault.kubernetes] + # ## The Kubernetes service account role name + # role_name = "" + # + # ## The Kubernetes service account token + # service_account_token = "" + + # [secretstores.vault.userpass] + # ## The Vault Userpass username + # username = "" + # + # ## The Vault Userpass password + # password = "" - ## The Secret ID for AppRole Authentication - secret = "" ``` [vault]: https://www.hashicorp.com/en/products/vault diff --git a/plugins/secretstores/vault/auth/approle.go b/plugins/secretstores/vault/auth/approle.go new file mode 100644 index 0000000000000..5982acb681c3c --- /dev/null +++ b/plugins/secretstores/vault/auth/approle.go @@ -0,0 +1,59 @@ +package auth + +import ( + "context" + "errors" + "fmt" + + vault "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/api/auth/approle" + + "github.com/influxdata/telegraf/config" +) + +type AppRole struct { + RoleID string `toml:"role_id"` + ResponseWrapped bool `toml:"response_wrapped"` + Secret config.Secret `toml:"secret"` +} + +// Init validates the auth method options and sets any necessary defaults +func (a *AppRole) Init() error { + if a.RoleID == "" { + return errors.New("approle role_id missing") + } + if a.Secret.Empty() { + return errors.New("approle secret missing") + } + return nil +} + +// Authenticate uses the provided configuration to authenticate to Vault +func (a *AppRole) Authenticate(v *vault.Client) (*vault.Secret, error) { + secret, err := a.Secret.Get() + if err != nil { + return nil, fmt.Errorf("getting secret failed: %w", err) + } + secretID := &approle.SecretID{FromString: secret.String()} + defer secret.Destroy() + + var opts []approle.LoginOption + if a.ResponseWrapped { + opts = append(opts, approle.WithWrappingToken()) + } + + appRoleAuth, err := approle.NewAppRoleAuth(a.RoleID, secretID, opts...) + if err != nil { + return nil, fmt.Errorf("unable to initialize AppRole auth method: %w", err) + } + + authInfo, err := v.Auth().Login(context.Background(), appRoleAuth) + if err != nil { + return nil, fmt.Errorf("unable to login to AppRole auth method: %w", err) + } + if authInfo == nil { + return nil, errors.New("no auth info was returned after login") + } + + return authInfo, nil +} diff --git a/plugins/secretstores/vault/auth/auth.go b/plugins/secretstores/vault/auth/auth.go new file mode 100644 index 0000000000000..08722702c020f --- /dev/null +++ b/plugins/secretstores/vault/auth/auth.go @@ -0,0 +1,11 @@ +package auth + +import vault "github.com/hashicorp/vault/api" + +type VaultAuth interface { + // Init validates the auth method options and sets any necessary defaults + Init() error + + // Authenticate uses the provided configuration to authenticate to Vault + Authenticate(*vault.Client) (*vault.Secret, error) +} diff --git a/plugins/secretstores/vault/auth/aws.go b/plugins/secretstores/vault/auth/aws.go new file mode 100644 index 0000000000000..05a30187e6980 --- /dev/null +++ b/plugins/secretstores/vault/auth/aws.go @@ -0,0 +1,116 @@ +package auth + +import ( + "context" + "errors" + "fmt" + + vault "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/api/auth/aws" +) + +type AwsIAM struct { + RoleName string `toml:"role_name"` + Region string `toml:"region"` + ServerIDHeader string `toml:"server_id_header"` +} + +// Init validates the auth method options and sets any necessary defaults +func (a *AwsIAM) Init() error { + if a.RoleName == "" { + return errors.New("aws iam role_name missing") + } + + if a.Region == "" { + a.Region = "us-east-1" + } + + return nil +} + +// Authenticate uses the provided configuration to authenticate to Vault +func (a *AwsIAM) Authenticate(v *vault.Client) (*vault.Secret, error) { + opts := []aws.LoginOption{ + aws.WithIAMAuth(), + aws.WithRole(a.RoleName), + aws.WithRegion(a.Region), + } + if a.ServerIDHeader != "" { + opts = append(opts, aws.WithIAMServerIDHeader(a.ServerIDHeader)) + } + + awsAuth, err := aws.NewAWSAuth(opts...) + if err != nil { + return nil, fmt.Errorf("unable to initialize AWS IAM auth method: %w", err) + } + + authInfo, err := v.Auth().Login(context.Background(), awsAuth) + if err != nil { + return nil, fmt.Errorf("unable to login to AWS IAM auth method: %w", err) + } + if authInfo == nil { + return nil, errors.New("no auth info was returned after login") + } + + return authInfo, nil +} + +type AwsEC2 struct { + RoleName string `toml:"role_name"` + Region string `toml:"region"` + SignatureType string `toml:"signature_type"` +} + +// Init validates the auth method options and sets any necessary defaults +func (a *AwsEC2) Init() error { + if a.RoleName == "" { + return errors.New("aws ec2 role_name missing") + } + + switch a.SignatureType { + case "": + a.SignatureType = "pkcs7" + case "pkcs7", "identity", "rsa2048": + default: + return fmt.Errorf("unknown signature type: %q", a.SignatureType) + } + + if a.Region == "" { + a.Region = "us-east-1" + } + + return nil +} + +// Authenticate uses the provided configuration to authenticate to Vault +func (a *AwsEC2) Authenticate(v *vault.Client) (*vault.Secret, error) { + opts := []aws.LoginOption{ + aws.WithEC2Auth(), + aws.WithRole(a.RoleName), + aws.WithRegion(a.Region), + } + + switch a.SignatureType { + case "pkcs7": + opts = append(opts, aws.WithPKCS7Signature()) + case "identity": + opts = append(opts, aws.WithIdentitySignature()) + case "rsa2048": + opts = append(opts, aws.WithRSA2048Signature()) + } + + awsAuth, err := aws.NewAWSAuth(opts...) + if err != nil { + return nil, fmt.Errorf("unable to initialize AWS EC2 auth method: %w", err) + } + + authInfo, err := v.Auth().Login(context.Background(), awsAuth) + if err != nil { + return nil, fmt.Errorf("unable to login to AWS EC2 auth method: %w", err) + } + if authInfo == nil { + return nil, errors.New("no auth info was returned after login") + } + + return authInfo, nil +} diff --git a/plugins/secretstores/vault/auth/azure.go b/plugins/secretstores/vault/auth/azure.go new file mode 100644 index 0000000000000..3e79d8f5caa70 --- /dev/null +++ b/plugins/secretstores/vault/auth/azure.go @@ -0,0 +1,49 @@ +package auth + +import ( + "context" + "errors" + "fmt" + + vault "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/api/auth/azure" +) + +type Azure struct { + RoleName string `toml:"role_name"` + ResourceURL string `toml:"resource_url"` +} + +// Init validates the auth method options and sets any necessary defaults +func (a *Azure) Init() error { + if a.RoleName == "" { + return errors.New("azure role_name missing") + } + + if a.ResourceURL == "" { + a.ResourceURL = "https://management.azure.com/" + } + + return nil +} + +// Authenticate uses the provided configuration to authenticate to Vault +func (a *Azure) Authenticate(v *vault.Client) (*vault.Secret, error) { + azureAuth, err := azure.NewAzureAuth( + a.RoleName, + azure.WithResource(a.ResourceURL), + ) + if err != nil { + return nil, fmt.Errorf("unable to initialize Azure auth method: %w", err) + } + + authInfo, err := v.Auth().Login(context.Background(), azureAuth) + if err != nil { + return nil, fmt.Errorf("unable to login to Azure auth method: %w", err) + } + if authInfo == nil { + return nil, errors.New("no auth info was returned after login") + } + + return authInfo, nil +} diff --git a/plugins/secretstores/vault/auth/kubernetes.go b/plugins/secretstores/vault/auth/kubernetes.go new file mode 100644 index 0000000000000..31caad2139568 --- /dev/null +++ b/plugins/secretstores/vault/auth/kubernetes.go @@ -0,0 +1,53 @@ +package auth + +import ( + "context" + "errors" + "fmt" + + vault "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/api/auth/kubernetes" + + "github.com/influxdata/telegraf/config" +) + +type Kubernetes struct { + RoleName string `toml:"role_name"` + ServiceAccountToken config.Secret `toml:"service_account_token"` +} + +// Init validates the auth method options and sets any necessary defaults +func (k *Kubernetes) Init() error { + if k.RoleName == "" { + return errors.New("kubernetes role_name missing") + } + if k.ServiceAccountToken.Empty() { + return errors.New("kubernetes service_account_token missing") + } + return nil +} + +// Authenticate uses the provided configuration to authenticate to Vault +func (k *Kubernetes) Authenticate(client *vault.Client) (*vault.Secret, error) { + secret, err := k.ServiceAccountToken.Get() + if err != nil { + return nil, fmt.Errorf("getting secret failed: %w", err) + } + opt := kubernetes.WithServiceAccountToken(secret.String()) + defer secret.Destroy() + + kubernetesAuth, err := kubernetes.NewKubernetesAuth(k.RoleName, opt) + if err != nil { + return nil, fmt.Errorf("unable to initialize Kubernetes auth method: %w", err) + } + + authInfo, err := client.Auth().Login(context.Background(), kubernetesAuth) + if err != nil { + return nil, fmt.Errorf("unable to login to Kubernetes auth method: %w", err) + } + if authInfo == nil { + return nil, errors.New("no auth info was returned after login") + } + + return authInfo, nil +} diff --git a/plugins/secretstores/vault/auth/userpass.go b/plugins/secretstores/vault/auth/userpass.go new file mode 100644 index 0000000000000..074d9202519f3 --- /dev/null +++ b/plugins/secretstores/vault/auth/userpass.go @@ -0,0 +1,53 @@ +package auth + +import ( + "context" + "errors" + "fmt" + + vault "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/api/auth/userpass" + + "github.com/influxdata/telegraf/config" +) + +type UserPass struct { + Username string `toml:"username"` + Password config.Secret `toml:"password"` +} + +// Init validates the auth method options and sets any necessary defaults +func (u *UserPass) Init() error { + if u.Username == "" { + return errors.New("userpass username missing") + } + if u.Password.Empty() { + return errors.New("userpass password missing") + } + return nil +} + +// Authenticate uses the provided configuration to authenticate to Vault +func (u *UserPass) Authenticate(v *vault.Client) (*vault.Secret, error) { + secret, err := u.Password.Get() + if err != nil { + return nil, fmt.Errorf("getting secret failed: %w", err) + } + password := &userpass.Password{FromString: secret.String()} + defer secret.Destroy() + + userPassAuth, err := userpass.NewUserpassAuth(u.Username, password) + if err != nil { + return nil, fmt.Errorf("unable to initialize Userpass auth method: %w", err) + } + + authInfo, err := v.Auth().Login(context.Background(), userPassAuth) + if err != nil { + return nil, fmt.Errorf("unable to login to Userpass auth method: %w", err) + } + if authInfo == nil { + return nil, errors.New("no auth info was returned after login") + } + + return authInfo, nil +} diff --git a/plugins/secretstores/vault/sample.conf b/plugins/secretstores/vault/sample.conf index 3e8db637a1245..0e42c44ac431e 100644 --- a/plugins/secretstores/vault/sample.conf +++ b/plugins/secretstores/vault/sample.conf @@ -26,12 +26,64 @@ ## By default will use the kv-v2 engine. # engine = "kv-v2" - [secretstores.vault.approle] - ## The Role ID for AppRole Authentication, a UUID string - role_id = "" + # [secretstores.vault.approle] + # ## The Role ID for AppRole Authentication, a UUID string + # role_id = "" + # + # ## Whether the Secret ID is configured to be response wrapped or not + # # response_wrapped = false + # + # ## The Secret ID for AppRole Authentication + # secret = "" - ## Whether the Secret ID is configured to be response wrapped or not - # response_wrapped = false + # [secretstores.vault.aws_ec2] + # ## The Role Name for AWS EC2 authentication + # role_name = "" + # + # ## The AWS region, defaulting to "us-east-1" if unset + # # region = "us-east-1" + # + # ## The signature type to use, defaulting to "pkcs7" + # ## Allowed options: "pkcs7", "identity", "rsa2048" + # # signature_type = "pkcs7" + + # ## Credentials will be set using the values in the environment variables: + # ## - AWS_ACCESS_KEY_ID + # ## - AWS_SECRET_ACCESS_KEY + # ## - AWS_SESSION_TOKEN + # ## To specify a path to a credentials file instead, set: + # ## - AWS_SHARED_CREDENTIALS_FILE + # [secretstores.vault.aws_iam] + # ## The Role Name for AWS IAM authentication + # role_name = "" + # + # ## The AWS region, defaulting to "us-east-1" if unset + # # region = "us-east-1" + # + # ## An optional server ID header to provide, with the key + # ## "X-Vault-AWS-IAM-Server-ID" + # # server_id_header = "" + + # [secretstores.vault.azure] + # ## The Role Name for Azure authentication + # role_name = "" + # + # ## The Azure Resource URL to use as the aud value on the JWT token to + # ## use rather than the default of Azure Public Cloud's ARM URL. + # ## Defaults to "https://management.azure.com/" + # # resource_url = "https://management.azure.com/" + + # [secretstores.vault.kubernetes] + # ## The Kubernetes service account role name + # role_name = "" + # + # ## The Kubernetes service account token + # service_account_token = "" + + # [secretstores.vault.userpass] + # ## The Vault Userpass username + # username = "" + # + # ## The Vault Userpass password + # password = "" - ## The Secret ID for AppRole Authentication - secret = "" diff --git a/plugins/secretstores/vault/vault.go b/plugins/secretstores/vault/vault.go index 7e85b37af1b31..f9604d235f3c0 100644 --- a/plugins/secretstores/vault/vault.go +++ b/plugins/secretstores/vault/vault.go @@ -10,33 +10,35 @@ import ( "slices" vault "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/api/auth/approle" "github.com/influxdata/telegraf" - "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/plugins/secretstores" + "github.com/influxdata/telegraf/plugins/secretstores/vault/auth" ) //go:embed sample.conf var sampleConfig string type Vault struct { - ID string `toml:"id"` - Address string `toml:"address"` - MountPath string `toml:"mount_path"` - SecretPath string `toml:"secret_path"` - Engine string `toml:"engine"` - AppRole *appRole `toml:"approle"` - + ID string `toml:"id"` + Address string `toml:"address"` + MountPath string `toml:"mount_path"` + SecretPath string `toml:"secret_path"` + Engine string `toml:"engine"` + + AppRole *auth.AppRole `toml:"approle"` + AwsEC2 *auth.AwsEC2 `toml:"aws_ec2"` + AwsIAM *auth.AwsIAM `toml:"aws_iam"` + Azure *auth.Azure `toml:"azure"` + Kubernetes *auth.Kubernetes `toml:"kubernetes"` + UserPass *auth.UserPass `toml:"userpass"` + + Log telegraf.Logger `toml:"-"` + + auth auth.VaultAuth client *vault.Client } -type appRole struct { - RoleID string `toml:"role_id"` - ResponseWrapped bool `toml:"response_wrapped"` - Secret config.Secret `toml:"secret"` -} - func (*Vault) SampleConfig() string { return sampleConfig } @@ -50,9 +52,10 @@ func (v *Vault) Init() error { return fmt.Errorf("unsupported engine: %s", v.Engine) } - if v.AppRole == nil { - return errors.New("approle configuration missing") + if err := v.validateAuth(); err != nil { + return err } + if v.ID == "" { return errors.New("id missing") } @@ -75,7 +78,25 @@ func (v *Vault) Init() error { v.client = client - return v.authenticate() + authInfo, err := v.auth.Authenticate(v.client) + if err != nil { + return err + } + + renewable, err := authInfo.TokenIsRenewable() + if err != nil { + v.Log.Errorf("failed to check if auth token is renewable: %v", err) + } + if renewable { + watcher, err := v.client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{Secret: authInfo}) + if err != nil { + v.Log.Errorf("unable to initialize Vault lifetime watcher: %v", err) + } else { + go watcher.Start() + } + } + + return nil } func (v *Vault) Get(key string) ([]byte, error) { @@ -131,39 +152,36 @@ func (v *Vault) GetResolver(key string) (telegraf.ResolveFunc, error) { return resolver, nil } -func (v *Vault) authenticate() error { - secret, err := v.AppRole.Secret.Get() - if err != nil { - return fmt.Errorf("getting secret failed: %w", err) +func (v *Vault) validateAuth() error { + var methods []auth.VaultAuth + if v.AppRole != nil { + methods = append(methods, v.AppRole) } - secretID := &approle.SecretID{FromString: secret.String()} - defer secret.Destroy() - - opts := make([]approle.LoginOption, 0) - if v.AppRole.ResponseWrapped { - opts = append(opts, approle.WithWrappingToken()) + if v.AwsEC2 != nil { + methods = append(methods, v.AwsEC2) } - - appRoleAuth, err := approle.NewAppRoleAuth(v.AppRole.RoleID, secretID, opts...) - if err != nil { - return fmt.Errorf("unable to initialize AppRole auth method: %w", err) + if v.AwsIAM != nil { + methods = append(methods, v.AwsIAM) } - - authInfo, err := v.client.Auth().Login(context.Background(), appRoleAuth) - if err != nil { - return fmt.Errorf("unable to login to AppRole auth method: %w", err) + if v.Azure != nil { + methods = append(methods, v.Azure) + } + if v.Kubernetes != nil { + methods = append(methods, v.Kubernetes) } - if authInfo == nil { - return errors.New("no auth info was returned after login") + if v.UserPass != nil { + methods = append(methods, v.UserPass) } - watcher, err := v.client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{Secret: authInfo}) - if err != nil { - return fmt.Errorf("unable to initialize Vault lifetime watcher: %w", err) + if len(methods) == 0 { + return errors.New("no auth method set") + } + if len(methods) > 1 { + return errors.New("must only specify one authentication method") } - go watcher.Start() - return nil + v.auth = methods[0] + return v.auth.Init() } func (v *Vault) getSecret() (*vault.KVSecret, error) { diff --git a/plugins/secretstores/vault/vault_test.go b/plugins/secretstores/vault/vault_test.go index 74ba7bdfeaca9..3450348cea0b5 100644 --- a/plugins/secretstores/vault/vault_test.go +++ b/plugins/secretstores/vault/vault_test.go @@ -14,6 +14,7 @@ import ( "github.com/testcontainers/testcontainers-go/modules/vault" "github.com/influxdata/telegraf/config" + "github.com/influxdata/telegraf/plugins/secretstores/vault/auth" ) func createContainer(t *testing.T, initCommands []string) (*vault.VaultContainer, func()) { @@ -155,7 +156,7 @@ func TestIntegrationKVv1(t *testing.T) { MountPath: mountPath, SecretPath: secretPath, Engine: "kv-v1", - AppRole: &appRole{ + AppRole: &auth.AppRole{ RoleID: getRoleID(t, container), Secret: config.NewSecret([]byte(getSecretID(t, container))), }, @@ -192,7 +193,7 @@ func TestIntegrationKVv2(t *testing.T) { Address: addr, MountPath: mountPath, SecretPath: secretPath, - AppRole: &appRole{ + AppRole: &auth.AppRole{ RoleID: getRoleID(t, container), Secret: config.NewSecret([]byte(getSecretID(t, container))), }, @@ -229,7 +230,7 @@ func TestIntegrationAppRoleSecretWrapped(t *testing.T) { Address: addr, MountPath: mountPath, SecretPath: secretPath, - AppRole: &appRole{ + AppRole: &auth.AppRole{ RoleID: getRoleID(t, container), Secret: config.NewSecret([]byte(getWrappedSecretID(t, container))), ResponseWrapped: true,