auth/vault: add Vault namespace support

New CLI flags:
- --iam-vault-namespace
- --iam-vault-auth-namespace
- --iam-vault-secret-storage-namespace

Behavior:
- Auth requests use the auth namespace
- KV operations use the secret storage namespace
- If a specific namespace is not set, the shared namespace is used
- With AppRole, different auth and secret namespaces are rejected
This commit is contained in:
Kim Henriksen
2025-10-03 23:33:34 +02:00
parent bef297f6ad
commit 45f55c2283
3 changed files with 197 additions and 120 deletions

View File

@@ -107,42 +107,45 @@ var (
)
type Opts struct {
RootAccount Account
Dir string
LDAPServerURL string
LDAPBindDN string
LDAPPassword string
LDAPQueryBase string
LDAPObjClasses string
LDAPAccessAtr string
LDAPSecretAtr string
LDAPRoleAtr string
LDAPUserIdAtr string
LDAPGroupIdAtr string
VaultEndpointURL string
VaultSecretStoragePath string
VaultAuthMethod string
VaultMountPath string
VaultRootToken string
VaultRoleId string
VaultRoleSecret string
VaultServerCert string
VaultClientCert string
VaultClientCertKey string
S3Access string
S3Secret string
S3Region string
S3Bucket string
S3Endpoint string
S3DisableSSlVerfiy bool
CacheDisable bool
CacheTTL int
CachePrune int
IpaHost string
IpaVaultName string
IpaUser string
IpaPassword string
IpaInsecure bool
RootAccount Account
Dir string
LDAPServerURL string
LDAPBindDN string
LDAPPassword string
LDAPQueryBase string
LDAPObjClasses string
LDAPAccessAtr string
LDAPSecretAtr string
LDAPRoleAtr string
LDAPUserIdAtr string
LDAPGroupIdAtr string
VaultEndpointURL string
VaultNamespace string
VaultSecretStoragePath string
VaultSecretStorageNamespace string
VaultAuthMethod string
VaultAuthNamespace string
VaultMountPath string
VaultRootToken string
VaultRoleId string
VaultRoleSecret string
VaultServerCert string
VaultClientCert string
VaultClientCertKey string
S3Access string
S3Secret string
S3Region string
S3Bucket string
S3Endpoint string
S3DisableSSlVerfiy bool
CacheDisable bool
CacheTTL int
CachePrune int
IpaHost string
IpaVaultName string
IpaUser string
IpaPassword string
IpaInsecure bool
}
func New(o *Opts) (IAMService, error) {
@@ -164,8 +167,8 @@ func New(o *Opts) (IAMService, error) {
fmt.Printf("initializing S3 IAM with '%v/%v'\n",
o.S3Endpoint, o.S3Bucket)
case o.VaultEndpointURL != "":
svc, err = NewVaultIAMService(o.RootAccount, o.VaultEndpointURL, o.VaultSecretStoragePath,
o.VaultAuthMethod, o.VaultMountPath, o.VaultRootToken, o.VaultRoleId, o.VaultRoleSecret,
svc, err = NewVaultIAMService(o.RootAccount, o.VaultEndpointURL, o.VaultNamespace, o.VaultSecretStoragePath, o.VaultSecretStorageNamespace,
o.VaultAuthMethod, o.VaultAuthNamespace, o.VaultMountPath, o.VaultRootToken, o.VaultRoleId, o.VaultRoleSecret,
o.VaultServerCert, o.VaultClientCert, o.VaultClientCertKey)
fmt.Printf("initializing Vault IAM with %q\n", o.VaultEndpointURL)
case o.IpaHost != "":

View File

@@ -38,15 +38,39 @@ type VaultIAMService struct {
creds schema.AppRoleLoginRequest
}
type VaultIAMNamespace struct {
Auth string
SecretStorage string
}
// Resolve empty specific namespaces to the fallback.
// Empty result means root namespace.
func resolveVaultNamespaces(authNamespace, secretStorageNamespace, fallback string) VaultIAMNamespace {
ns := VaultIAMNamespace{
Auth: authNamespace,
SecretStorage: secretStorageNamespace,
}
if ns.Auth == "" {
ns.Auth = fallback
}
if ns.SecretStorage == "" {
ns.SecretStorage = fallback
}
return ns
}
var _ IAMService = &VaultIAMService{}
func NewVaultIAMService(rootAcc Account, endpoint, secretStoragePath,
authMethod, mountPath, rootToken, roleID, roleSecret, serverCert,
func NewVaultIAMService(rootAcc Account, endpoint, namespace, secretStoragePath, secretStorageNamespace,
authMethod, authNamespace, mountPath, rootToken, roleID, roleSecret, serverCert,
clientCert, clientCertKey string) (IAMService, error) {
opts := []vault.ClientOption{
vault.WithAddress(endpoint),
vault.WithRequestTimeout(requestTimeout),
}
if serverCert != "" {
tls := vault.TLSConfiguration{}
@@ -80,6 +104,28 @@ func NewVaultIAMService(rootAcc Account, endpoint, secretStoragePath,
kvReqOpts = append(kvReqOpts, vault.WithMountPath(mountPath))
}
// Resolve namespaces using optional generic fallback "namespace"
ns := resolveVaultNamespaces(authNamespace, secretStorageNamespace, namespace)
// Guard: AppRole tokens are namespace scoped. If using AppRole and namespaces differ, error early.
// Root token can span namespaces because each request carries X-Vault-Namespace.
if rootToken == "" && ns.Auth != "" && ns.SecretStorage != "" && ns.Auth != ns.SecretStorage {
return nil, fmt.Errorf(
"approle tokens are namespace scoped. auth namespace %q and secret storage namespace %q differ. "+
"use the same namespace or authenticate with a root token",
ns.Auth, ns.SecretStorage,
)
}
// Apply namespaces to the correct request option sets.
// For root token we do not need an auth namespace since we are not logging in via auth.
if rootToken == "" && ns.Auth != "" {
authReqOpts = append(authReqOpts, vault.WithNamespace(ns.Auth))
}
if ns.SecretStorage != "" {
kvReqOpts = append(kvReqOpts, vault.WithNamespace(ns.SecretStorage))
}
creds := schema.AppRoleLoginRequest{
RoleId: roleID,
SecretId: roleSecret,
@@ -179,6 +225,10 @@ func (vt *VaultIAMService) CreateAccount(account Account) error {
if strings.Contains(err.Error(), "check-and-set") {
return ErrUserExists
}
if vault.IsErrorStatus(err, http.StatusForbidden) {
return fmt.Errorf("vault 403 permission denied on path %q. check KV mount path and policy. original: %w",
vt.secretStoragePath+"/"+account.Access, err)
}
return err
}
return nil

View File

@@ -37,51 +37,54 @@ import (
)
var (
port, admPort string
rootUserAccess string
rootUserSecret string
region string
admCertFile, admKeyFile string
certFile, keyFile string
kafkaURL, kafkaTopic, kafkaKey string
natsURL, natsTopic string
rabbitmqURL, rabbitmqExchange string
rabbitmqRoutingKey string
eventWebhookURL string
eventConfigFilePath string
logWebhookURL, accessLog string
adminLogFile string
healthPath string
virtualDomain string
debug bool
keepAlive bool
pprof string
quiet bool
readonly bool
iamDir string
ldapURL, ldapBindDN, ldapPassword string
ldapQueryBase, ldapObjClasses string
ldapAccessAtr, ldapSecAtr, ldapRoleAtr string
ldapUserIdAtr, ldapGroupIdAtr string
vaultEndpointURL, vaultSecretStoragePath string
vaultAuthMethod, vaultMountPath string
vaultRootToken, vaultRoleId string
vaultRoleSecret, vaultServerCert string
vaultClientCert, vaultClientCertKey string
s3IamAccess, s3IamSecret string
s3IamRegion, s3IamBucket string
s3IamEndpoint string
s3IamSslNoVerify bool
iamCacheDisable bool
iamCacheTTL int
iamCachePrune int
metricsService string
statsdServers string
dogstatsServers string
ipaHost, ipaVaultName string
ipaUser, ipaPassword string
ipaInsecure bool
iamDebug bool
port, admPort string
rootUserAccess string
rootUserSecret string
region string
admCertFile, admKeyFile string
certFile, keyFile string
kafkaURL, kafkaTopic, kafkaKey string
natsURL, natsTopic string
rabbitmqURL, rabbitmqExchange string
rabbitmqRoutingKey string
eventWebhookURL string
eventConfigFilePath string
logWebhookURL, accessLog string
adminLogFile string
healthPath string
virtualDomain string
debug bool
keepAlive bool
pprof string
quiet bool
readonly bool
iamDir string
ldapURL, ldapBindDN, ldapPassword string
ldapQueryBase, ldapObjClasses string
ldapAccessAtr, ldapSecAtr, ldapRoleAtr string
ldapUserIdAtr, ldapGroupIdAtr string
vaultEndpointURL, vaultNamespace string
vaultSecretStoragePath string
vaultSecretStorageNamespace string
vaultAuthMethod, vaultAuthNamespace string
vaultMountPath string
vaultRootToken, vaultRoleId string
vaultRoleSecret, vaultServerCert string
vaultClientCert, vaultClientCertKey string
s3IamAccess, s3IamSecret string
s3IamRegion, s3IamBucket string
s3IamEndpoint string
s3IamSslNoVerify bool
iamCacheDisable bool
iamCacheTTL int
iamCachePrune int
metricsService string
statsdServers string
dogstatsServers string
ipaHost, ipaVaultName string
ipaUser, ipaPassword string
ipaInsecure bool
iamDebug bool
)
var (
@@ -405,18 +408,36 @@ func initFlags() []cli.Flag {
EnvVars: []string{"VGW_IAM_VAULT_ENDPOINT_URL"},
Destination: &vaultEndpointURL,
},
&cli.StringFlag{
Name: "iam-vault-namespace",
Usage: "vault server namespace",
EnvVars: []string{"VGW_IAM_VAULT_NAMESPACE"},
Destination: &vaultNamespace,
},
&cli.StringFlag{
Name: "iam-vault-secret-storage-path",
Usage: "vault server secret storage path",
EnvVars: []string{"VGW_IAM_VAULT_SECRET_STORAGE_PATH"},
Destination: &vaultSecretStoragePath,
},
&cli.StringFlag{
Name: "iam-vault-secret-storage-namespace",
Usage: "vault server secret storage namespace",
EnvVars: []string{"VGW_IAM_VAULT_SECRET_STORAGE_NAMESPACE"},
Destination: &vaultSecretStorageNamespace,
},
&cli.StringFlag{
Name: "iam-vault-auth-method",
Usage: "vault server auth method",
EnvVars: []string{"VGW_IAM_VAULT_AUTH_METHOD"},
Destination: &vaultAuthMethod,
},
&cli.StringFlag{
Name: "iam-vault-auth-namespace",
Usage: "vault server auth namespace",
EnvVars: []string{"VGW_IAM_VAULT_AUTH_NAMESPACE"},
Destination: &vaultAuthNamespace,
},
&cli.StringFlag{
Name: "iam-vault-mount-path",
Usage: "vault server mount path",
@@ -650,41 +671,44 @@ func runGateway(ctx context.Context, be backend.Backend) error {
Secret: rootUserSecret,
Role: auth.RoleAdmin,
},
Dir: iamDir,
LDAPServerURL: ldapURL,
LDAPBindDN: ldapBindDN,
LDAPPassword: ldapPassword,
LDAPQueryBase: ldapQueryBase,
LDAPObjClasses: ldapObjClasses,
LDAPAccessAtr: ldapAccessAtr,
LDAPSecretAtr: ldapSecAtr,
LDAPRoleAtr: ldapRoleAtr,
LDAPUserIdAtr: ldapUserIdAtr,
LDAPGroupIdAtr: ldapGroupIdAtr,
VaultEndpointURL: vaultEndpointURL,
VaultSecretStoragePath: vaultSecretStoragePath,
VaultAuthMethod: vaultAuthMethod,
VaultMountPath: vaultMountPath,
VaultRootToken: vaultRootToken,
VaultRoleId: vaultRoleId,
VaultRoleSecret: vaultRoleSecret,
VaultServerCert: vaultServerCert,
VaultClientCert: vaultClientCert,
VaultClientCertKey: vaultClientCertKey,
S3Access: s3IamAccess,
S3Secret: s3IamSecret,
S3Region: s3IamRegion,
S3Bucket: s3IamBucket,
S3Endpoint: s3IamEndpoint,
S3DisableSSlVerfiy: s3IamSslNoVerify,
CacheDisable: iamCacheDisable,
CacheTTL: iamCacheTTL,
CachePrune: iamCachePrune,
IpaHost: ipaHost,
IpaVaultName: ipaVaultName,
IpaUser: ipaUser,
IpaPassword: ipaPassword,
IpaInsecure: ipaInsecure,
Dir: iamDir,
LDAPServerURL: ldapURL,
LDAPBindDN: ldapBindDN,
LDAPPassword: ldapPassword,
LDAPQueryBase: ldapQueryBase,
LDAPObjClasses: ldapObjClasses,
LDAPAccessAtr: ldapAccessAtr,
LDAPSecretAtr: ldapSecAtr,
LDAPRoleAtr: ldapRoleAtr,
LDAPUserIdAtr: ldapUserIdAtr,
LDAPGroupIdAtr: ldapGroupIdAtr,
VaultEndpointURL: vaultEndpointURL,
VaultNamespace: vaultNamespace,
VaultSecretStoragePath: vaultSecretStoragePath,
VaultSecretStorageNamespace: vaultSecretStorageNamespace,
VaultAuthMethod: vaultAuthMethod,
VaultAuthNamespace: vaultAuthNamespace,
VaultMountPath: vaultMountPath,
VaultRootToken: vaultRootToken,
VaultRoleId: vaultRoleId,
VaultRoleSecret: vaultRoleSecret,
VaultServerCert: vaultServerCert,
VaultClientCert: vaultClientCert,
VaultClientCertKey: vaultClientCertKey,
S3Access: s3IamAccess,
S3Secret: s3IamSecret,
S3Region: s3IamRegion,
S3Bucket: s3IamBucket,
S3Endpoint: s3IamEndpoint,
S3DisableSSlVerfiy: s3IamSslNoVerify,
CacheDisable: iamCacheDisable,
CacheTTL: iamCacheTTL,
CachePrune: iamCachePrune,
IpaHost: ipaHost,
IpaVaultName: ipaVaultName,
IpaUser: ipaUser,
IpaPassword: ipaPassword,
IpaInsecure: ipaInsecure,
})
if err != nil {
return fmt.Errorf("setup iam: %w", err)