diff --git a/internal/keystore/vault/client.go b/internal/keystore/vault/client.go index e5526d47..42625db8 100644 --- a/internal/keystore/vault/client.go +++ b/internal/keystore/vault/client.go @@ -71,7 +71,15 @@ func (c *client) CheckStatus(ctx context.Context, delay time.Duration) { // To renew the auth. token see: client.RenewToken(...). func (c *client) AuthenticateWithAppRole(login *AppRole) authFunc { return func() (*vaultapi.Secret, error) { - secret, err := c.Logical().Write(path.Join("auth", login.Engine, "login"), map[string]interface{}{ + client := c.Client + switch { + case login.Namespace == "/": // Treat '/' as the root namespace + client = client.WithNamespace("") // Clear namespace + case login.Namespace != "": + client = client.WithNamespace(login.Namespace) + } + + secret, err := client.Logical().Write(path.Join("auth", login.Engine, "login"), map[string]interface{}{ "role_id": login.ID, "secret_id": login.Secret, }) @@ -88,7 +96,15 @@ func (c *client) AuthenticateWithAppRole(login *AppRole) authFunc { func (c *client) AuthenticateWithK8S(login *Kubernetes) authFunc { return func() (*vaultapi.Secret, error) { - secret, err := c.Logical().Write(path.Join("auth", login.Engine, "login"), map[string]interface{}{ + client := c.Client + switch { + case login.Namespace == "/": // Treat '/' as the root namespace + client = client.WithNamespace("") // Clear namespace + case login.Namespace != "": + client = client.WithNamespace(login.Namespace) + } + + secret, err := client.Logical().Write(path.Join("auth", login.Engine, "login"), map[string]interface{}{ "role": login.Role, "jwt": login.JWT, }) diff --git a/internal/keystore/vault/config.go b/internal/keystore/vault/config.go index 695cc6f4..c9c34904 100644 --- a/internal/keystore/vault/config.go +++ b/internal/keystore/vault/config.go @@ -49,6 +49,19 @@ type AppRole struct { // mounted at arbitrary paths. Engine string + // Namespace is the Vault namespace in which the AppRole + // authentication is performed. It can be used to authenticate + // in a different namespace compared to the secret engine + // namespace. For example, authenticate within the root + // namespace but use a team-specific namespace for the secret + // engine. + // + // If empty, the VaultKeyStore namespace is used, if set. + // A single "/" is treated as alias for the Vault root + // namespace such that no namespace header is sent as part + // of the request. + Namespace string + // ID is the AppRole authentication ID ID string @@ -62,9 +75,10 @@ func (a *AppRole) Clone() *AppRole { return nil } return &AppRole{ - Engine: a.Engine, - ID: a.ID, - Secret: a.Secret, + Engine: a.Engine, + Namespace: a.Namespace, + ID: a.ID, + Secret: a.Secret, } } @@ -81,6 +95,19 @@ type Kubernetes struct { // mounted at arbitrary paths. Engine string + // Namespace is the Vault namespace in which the Kubernetes + // authentication is performed. It can be used to authenticate + // in a different namespace compared to the secret engine + // namespace. For example, authenticate within the root + // namespace but use a team-specific namespace for the secret + // engine. + // + // If empty, the VaultKeyStore namespace is used, if set. + // A single "/" is treated as alias for the Vault root + // namespace such that no namespace header is sent as part + // of the request. + Namespace string + // Role is the JWT role. Role string @@ -94,9 +121,10 @@ func (k *Kubernetes) Clone() *Kubernetes { return nil } return &Kubernetes{ - Engine: k.Engine, - Role: k.Role, - JWT: k.JWT, + Engine: k.Engine, + Namespace: k.Namespace, + Role: k.Role, + JWT: k.JWT, } } diff --git a/kesconf/config.go b/kesconf/config.go index 4e9040b1..a68a72f9 100644 --- a/kesconf/config.go +++ b/kesconf/config.go @@ -99,15 +99,17 @@ type ymlFile struct { } AppRole *struct { - Engine env[string] `yaml:"engine"` - ID env[string] `yaml:"id"` - Secret env[string] `yaml:"secret"` + Engine env[string] `yaml:"engine"` + Namespace env[string] `yaml:"namespace"` + ID env[string] `yaml:"id"` + Secret env[string] `yaml:"secret"` } `yaml:"approle"` Kubernetes *struct { - Engine env[string] `yaml:"engine"` - Role env[string] `yaml:"role"` - JWT env[string] `yaml:"jwt"` // Can be either a JWT or a path to a file containing a JWT + Engine env[string] `yaml:"engine"` + Namespace env[string] `yaml:"namespace"` + Role env[string] `yaml:"role"` + JWT env[string] `yaml:"jwt"` // Can be either a JWT or a path to a file containing a JWT } `yaml:"kubernetes"` TLS struct { @@ -463,16 +465,18 @@ func ymlToKeyStore(y *ymlFile) (KeyStore, error) { } if y.KeyStore.Vault.AppRole != nil { s.AppRole = &VaultAppRoleAuth{ - Engine: y.KeyStore.Vault.AppRole.Engine.Value, - ID: y.KeyStore.Vault.AppRole.ID.Value, - Secret: y.KeyStore.Vault.AppRole.Secret.Value, + Engine: y.KeyStore.Vault.AppRole.Engine.Value, + Namespace: y.KeyStore.Vault.AppRole.Namespace.Value, + ID: y.KeyStore.Vault.AppRole.ID.Value, + Secret: y.KeyStore.Vault.AppRole.Secret.Value, } } if y.KeyStore.Vault.Kubernetes != nil { s.Kubernetes = &VaultKubernetesAuth{ - Engine: y.KeyStore.Vault.Kubernetes.Engine.Value, - JWT: y.KeyStore.Vault.Kubernetes.JWT.Value, - Role: y.KeyStore.Vault.Kubernetes.Role.Value, + Engine: y.KeyStore.Vault.Kubernetes.Engine.Value, + Namespace: y.KeyStore.Vault.Kubernetes.Namespace.Value, + JWT: y.KeyStore.Vault.Kubernetes.JWT.Value, + Role: y.KeyStore.Vault.Kubernetes.Role.Value, } } if y.KeyStore.Vault.Transit != nil { diff --git a/kesconf/file.go b/kesconf/file.go index 858965c7..6913bc1f 100644 --- a/kesconf/file.go +++ b/kesconf/file.go @@ -461,6 +461,19 @@ type VaultAppRoleAuth struct { // If empty, defaults to "approle". Engine string + // Namespace is the Vault namespace in which the AppRole + // authentication is performed. It can be used to authenticate + // in a different namespace compared to the secret engine + // namespace. For example, authenticate within the root + // namespace but use a team-specific namespace for the secret + // engine. + // + // If empty, the VaultKeyStore namespace is used, if set. + // A single "/" is treated as alias for the Vault root + // namespace such that no namespace header is sent as part + // of the request. + Namespace string + // AppRoleID is the AppRole access ID for authenticating // to Hashicorp Vault via the AppRole method. ID string @@ -477,6 +490,19 @@ type VaultKubernetesAuth struct { // If empty, defaults to "kubernetes". Engine string + // Namespace is the Vault namespace in which the Kubernetes + // authentication is performed. It can be used to authenticate + // in a different namespace compared to the secret engine + // namespace. For example, authenticate within the root + // namespace but use a team-specific namespace for the secret + // engine. + // + // If empty, the VaultKeyStore namespace is used, if set. + // A single "/" is treated as alias for the Vault root + // namespace such that no namespace header is sent as part + // of the request. + Namespace string + // KubernetesRole is the login role for authenticating via the // kubernetes authentication method. Role string @@ -519,16 +545,18 @@ func (s *VaultKeyStore) Connect(ctx context.Context) (kes.KeyStore, error) { } if s.AppRole != nil { c.AppRole = &vault.AppRole{ - Engine: s.AppRole.Engine, - ID: s.AppRole.ID, - Secret: s.AppRole.Secret, + Engine: s.AppRole.Engine, + Namespace: s.AppRole.Namespace, + ID: s.AppRole.ID, + Secret: s.AppRole.Secret, } } if s.Kubernetes != nil { c.K8S = &vault.Kubernetes{ - Engine: s.Kubernetes.Engine, - Role: s.Kubernetes.Role, - JWT: s.Kubernetes.JWT, + Engine: s.Kubernetes.Engine, + Namespace: s.Kubernetes.Namespace, + Role: s.Kubernetes.Role, + JWT: s.Kubernetes.JWT, } } if s.Transit != nil { diff --git a/server-config.yaml b/server-config.yaml index 22e9abe7..a486b5c3 100644 --- a/server-config.yaml +++ b/server-config.yaml @@ -249,15 +249,15 @@ keystore: engine: "" # The path of the transit engine - e.g. "my-transit". If empty, defaults to: transit (Vault default) key: "" # The key name that should be used to encrypt entries stored on the K/V engine. approle: # AppRole credentials. See: https://www.vaultproject.io/docs/auth/approle.html - engine: "" # The path of the AppRole engine - e.g. authenticate. If empty, defaults to: approle. (Vault default) - id: "" # Your AppRole Role ID - secret: "" # Your AppRole Secret ID - retry: 15s # Duration until the server tries to re-authenticate after connection loss. + namespace: "" # Optional Vault namespace used just for authentication. A single "/" is an alias for the Vault root namespace. + engine: "" # The path of the AppRole engine - e.g. authenticate. If empty, defaults to: approle. (Vault default) + id: "" # Your AppRole Role ID + secret: "" # Your AppRole Secret ID kubernetes: # Kubernetes credentials. See: https://www.vaultproject.io/docs/auth/kubernetes - engine: "" # The path of the Kubernetes engine e.g. authenticate. If empty, defaults to: kubernetes. (Vault default) - role: "" # The Kubernetes JWT role - jwt: "" # Either the JWT provided by K8S or a path to a K8S secret containing the JWT. - retry: 15s # Duration until the server tries to re-authenticate after connection loss. + namespace: "" # Optional Vault namespace used just for authentication. A single "/" is an alias for the Vault root namespace. + engine: "" # The path of the Kubernetes engine e.g. authenticate. If empty, defaults to: kubernetes. (Vault default) + role: "" # The Kubernetes JWT role + jwt: "" # Either the JWT provided by K8S or a path to a K8S secret containing the JWT. tls: # The Vault client TLS configuration for mTLS authentication and certificate verification key: "" # Path to the TLS client private key for mTLS authentication to Vault cert: "" # Path to the TLS client certificate for mTLS authentication to Vault