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