Documentation
¶
Overview ¶
Package credentials provides AES-256-GCM encryption for cloud account credentials and helpers for resolving provider-specific credential structs.
Index ¶
- Constants
- Variables
- func BuildAzureFederatedCredential(signer oidc.Signer, issuerURL string, tenantID string, clientID string) (azcore.TokenCredential, error)
- func BuildGCPFederatedCredential(ctx context.Context, signer oidc.Signer, issuerURL string, audience string, ...) (oauth2.TokenSource, error)
- func Decrypt(key []byte, blob string) ([]byte, error)
- func DevKey() []byte
- func Encrypt(key, plaintext []byte) (string, error)
- func LoadKey(ctx context.Context, resolver secrets.Resolver) (key []byte, source string, err error)
- func ResolveAWSCredentialProvider(ctx context.Context, account *config.CloudAccount, store CredentialStore, ...) (aws.CredentialsProvider, error)
- func ResolveAWSCredentialProviderWithOpts(ctx context.Context, account *config.CloudAccount, store CredentialStore, ...) (aws.CredentialsProvider, error)
- func ResolveAzureTokenCredential(ctx context.Context, account *config.CloudAccount, store CredentialStore) (azcore.TokenCredential, error)
- func ResolveAzureTokenCredentialWithOpts(ctx context.Context, account *config.CloudAccount, store CredentialStore, ...) (azcore.TokenCredential, error)
- func ResolveGCPCredentials(ctx context.Context, account *config.CloudAccount, store CredentialStore) ([]byte, error)
- func ResolveGCPTokenSource(ctx context.Context, account *config.CloudAccount, store CredentialStore) (oauth2.TokenSource, error)
- func ResolveGCPTokenSourceWithOpts(ctx context.Context, account *config.CloudAccount, store CredentialStore, ...) (oauth2.TokenSource, error)
- func VerifyHostAccount(ctx context.Context, account *config.CloudAccount, ...) error
- type AWSConfigError
- type AWSCredentials
- type AWSResolveOptions
- type AccountLookupFunc
- type AzureCredentials
- type AzureResolveOptions
- type CallerIdentityClient
- type CredentialStore
- type GCPResolveOptions
- type STSClient
- type STSClientFactory
Constants ¶
const ( EnvSecretARN = "CREDENTIAL_ENCRYPTION_KEY_SECRET_ARN" // #nosec G101 -- env var name for AWS Secrets Manager ARN; not a credential value EnvSecretName = "CREDENTIAL_ENCRYPTION_KEY_SECRET_NAME" // #nosec G101 -- env var name for Azure Key Vault secret; not a credential value EnvSecretID = "CREDENTIAL_ENCRYPTION_KEY_SECRET_ID" // #nosec G101 -- env var name for GCP Secret Manager secret; not a credential value EnvRawKey = "CREDENTIAL_ENCRYPTION_KEY" // Raw 64-char hex (ops/dev) EnvAllowDev = "CREDENTIAL_ENCRYPTION_ALLOW_DEV_KEY" // 1 = permit zero-key fallback )
Env var names used by LoadKey, in priority order.
const ( CredTypeAWSAccessKeys = "aws_access_keys" CredTypeAzureClientSecret = "azure_client_secret" CredTypeGCPServiceAccount = "gcp_service_account" // #nosec G101 -- credential type name constant; not a credential value CredTypeGCPWIFConfig = "gcp_workload_identity_config" // #nosec G101 -- credential type name constant; not a credential value )
Credential type constants used as credType in CredentialStore.
const ProblemNotStored = "not stored"
ProblemNotStored is the AWSConfigError.Problem for a credential that has not been saved for the account.
Variables ¶
var ErrMultipleKeys = errors.New("credentials: multiple encryption-key env vars set; configure exactly one")
ErrMultipleKeys is returned by LoadKey when more than one key-source env var is set simultaneously. Ambiguous key selection is treated as a hard startup error so misconfiguration is caught immediately rather than silently using the first-priority source.
var ErrNoKey = errors.New("credentials: no credential encryption key configured")
ErrNoKey is returned by LoadKey when no encryption key is configured and CREDENTIAL_ENCRYPTION_ALLOW_DEV_KEY is not set.
var ErrNotHostAccount = errors.New("credentials: aws_role_arn is empty but the account is not confirmed as the CUDly host account; set aws_role_arn")
ErrNotHostAccount means a role_arn account with no role ARN could not be confirmed as the AWS account CUDly's own credentials belong to. Its text is stored in tenant-visible rows, so it never carries the host account id or raw STS errors; those are logged.
Functions ¶
func BuildAzureFederatedCredential ¶
func BuildAzureFederatedCredential( signer oidc.Signer, issuerURL string, tenantID string, clientID string, ) (azcore.TokenCredential, error)
BuildAzureFederatedCredential returns an azcore.TokenCredential that authenticates to Azure AD using a federated identity credential. On each call it mints a fresh JWT using the provided Signer, sets its issuer to the CUDly deployment URL, and hands it to azidentity.NewClientAssertionCredential which handles the actual exchange with the Azure token endpoint.
The target Azure AD app registration must have a federated identity credential configured with:
issuer = <issuerURL> subject = cudly-controller audience = api://AzureADTokenExchange
— all three are checked by Azure AD when validating the assertion.
No private key, cert, or client secret is stored anywhere — the signing happens inside the cloud KMS the Signer wraps.
func BuildGCPFederatedCredential ¶
func BuildGCPFederatedCredential( ctx context.Context, signer oidc.Signer, issuerURL string, audience string, serviceAccountEmail string, ) (oauth2.TokenSource, error)
BuildGCPFederatedCredential returns an oauth2.TokenSource that authenticates to GCP using a federated Workload Identity Pool provider bound to CUDly's OIDC issuer. Each call to Token() mints a fresh KMS-signed JWT, exchanges it at sts.googleapis.com for a GCP federated token, then impersonates the target service account via iamcredentials.googleapis.com to get an access token scoped to cloud-platform.
No service-account key JSON or any other long-lived secret is stored or read by this path — the signing happens inside the cloud KMS the Signer wraps.
func DevKey ¶
func DevKey() []byte
DevKey returns the all-zero 32-byte dev key. Exposed for the rekey migration command (cmd/rekey) which needs to detect rows encrypted under it without going through the LoadKey env-var path.
func Encrypt ¶
Encrypt encrypts plaintext using AES-256-GCM. Returns a string in the format "<base64url(nonce)>.<base64url(ciphertext)>".
func LoadKey ¶
LoadKey loads the 32-byte AES-256 key for tenant credential encryption. First non-empty wins, in this order:
- CREDENTIAL_ENCRYPTION_KEY_SECRET_ARN → resolver.GetSecret(arn) (AWS)
- CREDENTIAL_ENCRYPTION_KEY_SECRET_NAME → resolver.GetSecret(name) (Azure)
- CREDENTIAL_ENCRYPTION_KEY_SECRET_ID → resolver.GetSecret(id) (GCP)
- CREDENTIAL_ENCRYPTION_KEY → raw 64-char hex
If none are set, returns ErrNoKey unless CREDENTIAL_ENCRYPTION_ALLOW_DEV_KEY=1 is set (in which case the all-zero dev key is returned with a loud warning).
The returned source string is the env var name that resolved (for logging). Callers MUST NOT log the key value.
Result is memoized via sync.Once for the process lifetime; tests reset the cache via resetKeyCacheForTest() in this package's test files.
func ResolveAWSCredentialProvider ¶
func ResolveAWSCredentialProvider( ctx context.Context, account *config.CloudAccount, store CredentialStore, stsClient STSClient, ) (aws.CredentialsProvider, error)
ResolveAWSCredentialProvider is a back-compat wrapper that calls ResolveAWSCredentialProviderWithOpts with empty options. New callers should use the WithOpts variant so bastion mode can self-resolve.
func ResolveAWSCredentialProviderWithOpts ¶
func ResolveAWSCredentialProviderWithOpts( ctx context.Context, account *config.CloudAccount, store CredentialStore, stsClient STSClient, opts AWSResolveOptions, ) (aws.CredentialsProvider, error)
ResolveAWSCredentialProviderWithOpts returns an aws.CredentialsProvider for the account.
Logic per auth mode:
- access_keys: load stored key pair → static credentials provider
- role_arn: use ambient (instance role / env) → STS AssumeRole
- bastion: load bastion account creds → STS AssumeRole into target (requires opts.AccountLookup + opts.STSClientFactory)
- workload_identity_federation: token file → STS AssumeRoleWithWebIdentity
func ResolveAzureTokenCredential ¶
func ResolveAzureTokenCredential( ctx context.Context, account *config.CloudAccount, store CredentialStore, ) (azcore.TokenCredential, error)
ResolveAzureTokenCredential is a convenience wrapper that passes zero-valued options to ResolveAzureTokenCredentialWithOpts. Since the legacy cert-based WIF path has been removed, this entry point can only resolve client_secret and managed_identity accounts — WIF accounts require opts.Signer + IssuerURL via the With-opts variant.
func ResolveAzureTokenCredentialWithOpts ¶
func ResolveAzureTokenCredentialWithOpts( ctx context.Context, account *config.CloudAccount, store CredentialStore, opts AzureResolveOptions, ) (azcore.TokenCredential, error)
ResolveAzureTokenCredentialWithOpts is like ResolveAzureTokenCredential but accepts per-deployment options. For workload_identity_federation accounts, AzureResolveOptions.Signer and .IssuerURL must both be set so BuildAzureFederatedCredential can mint the KMS-signed client assertion Azure AD expects — there is no fallback path.
Routes by AzureAuthMode:
- managed_identity → ManagedIdentityCredential (no stored cred needed)
- workload_identity_federation → federated credential via opts.Signer
- client_secret (default) → loads stored secret and returns ClientSecretCredential
func ResolveGCPCredentials ¶
func ResolveGCPCredentials(ctx context.Context, account *config.CloudAccount, store CredentialStore) ([]byte, error)
ResolveGCPCredentials decrypts and returns the GCP service account JSON for the account.
func ResolveGCPTokenSource ¶
func ResolveGCPTokenSource(ctx context.Context, account *config.CloudAccount, store CredentialStore) (oauth2.TokenSource, error)
ResolveGCPTokenSource is the legacy entry point — calls ResolveGCPTokenSourceWithOpts with zero-valued options so existing cert-based / stored-JSON accounts keep working.
func ResolveGCPTokenSourceWithOpts ¶
func ResolveGCPTokenSourceWithOpts( ctx context.Context, account *config.CloudAccount, store CredentialStore, opts GCPResolveOptions, ) (oauth2.TokenSource, error)
ResolveGCPTokenSourceWithOpts is like ResolveGCPTokenSource but accepts per-deployment options. When opts.Signer and opts.IssuerURL are both set, accounts in workload_identity_federation mode with a gcp_wif_audience field and no stored JSON config are routed through BuildGCPFederatedCredential (the secret-free path). Existing stored-JSON accounts keep working.
Routes by GCPAuthMode:
- application_default → returns (nil, nil); caller uses ADC.
- workload_identity_federation → federated (if no stored cred and signer+issuer+audience present) or legacy stored-JSON.
- service_account_key (or empty) → stored-JSON path.
func VerifyHostAccount ¶
func VerifyHostAccount(ctx context.Context, account *config.CloudAccount, hostSTS CallerIdentityClient) error
VerifyHostAccount confirms account.ExternalID is the AWS account that hostSTS, an STS client signed with the host's ambient credentials, runs as. Every failure returns ErrNotHostAccount unwrapped; the detail goes to the log only.
Types ¶
type AWSConfigError ¶
type AWSConfigError struct {
Field string
Problem string
// contains filtered or unexported fields
}
AWSConfigError reports an AWS account row whose configuration the resolver cannot use. Field and Problem are fixed, tenant-safe strings (a setting name and what is wrong with it) that callers may show to users; Error() keeps the detailed log-oriented text the resolver has always returned.
func (*AWSConfigError) Error ¶
func (e *AWSConfigError) Error() string
type AWSCredentials ¶
AWSCredentials holds resolved AWS access key credentials.
func (*AWSCredentials) String ¶
func (c *AWSCredentials) String() string
String returns a redacted representation to prevent accidental logging.
type AWSResolveOptions ¶
type AWSResolveOptions struct {
AccountLookup AccountLookupFunc
STSClientFactory STSClientFactory
// AmbientProvider is the host Lambda / EC2 instance credentials provider.
// Required when resolving a Self account (role_arn with empty AWSRoleARN).
AmbientProvider aws.CredentialsProvider
// AmbientSTS is an STS client signed with AmbientProvider, used to check
// a Self account's external_id against the host identity.
AmbientSTS CallerIdentityClient
}
AWSResolveOptions holds optional dependencies for the AWS credential resolver. The bastion path needs both AccountLookup and STSClientFactory to self-resolve correctly; without them, bastion mode falls back to the pre-self-loading behavior (trusts the caller-supplied STS client) for backward compatibility.
AmbientProvider, when set, is returned for role_arn accounts whose AWSRoleARN is empty (the "Self" account shape: auth_mode=role_arn with no role ARN), once AmbientSTS confirms the account is the host. This mirrors the scheduler's collectAWSForAccount logic.
type AccountLookupFunc ¶
AccountLookupFunc resolves a CloudAccount by ID. Used by the bastion path so the resolver can self-load the bastion account's credentials rather than trust a caller-supplied STS client.
type AzureCredentials ¶
type AzureCredentials struct {
ClientSecret string //nolint:gosec // G117: intentional Azure client-secret field crossing the secure-store boundary; value is resolved from the credential store and used directly for API authentication
}
AzureCredentials holds resolved Azure service principal credentials.
func ResolveAzureCredentials ¶
func ResolveAzureCredentials(ctx context.Context, account *config.CloudAccount, store CredentialStore) (*AzureCredentials, error)
ResolveAzureCredentials decrypts and returns the Azure client secret for the account.
func (*AzureCredentials) String ¶
func (c *AzureCredentials) String() string
String returns a redacted representation.
type AzureResolveOptions ¶
type AzureResolveOptions struct {
// Signer is the OIDC issuer signer for this CUDly deployment.
// Non-nil signals that the deployment supports federated identity
// credentials.
Signer oidc.Signer
// IssuerURL is the base URL at which this deployment publishes
// /.well-known/openid-configuration. Must match what the Azure AD
// federated credential is registered with.
IssuerURL string
}
AzureResolveOptions carries per-deployment wiring that ResolveAzureTokenCredential needs to pick the federated (secret-free) path. A zero value selects the legacy cert-based path for backward compatibility with existing accounts.
type CallerIdentityClient ¶
type CallerIdentityClient interface {
GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error)
}
CallerIdentityClient is satisfied by *sts.Client.
type CredentialStore ¶
type CredentialStore interface {
// SaveCredential encrypts payload and persists it for accountID/credType.
// If a record for the same (accountID, credType) already exists it is replaced.
SaveCredential(ctx context.Context, accountID, credType string, payload []byte) error
// LoadRaw decrypts and returns the raw credential bytes.
// Returns nil, nil when no credential is stored for the given (accountID, credType).
LoadRaw(ctx context.Context, accountID, credType string) ([]byte, error)
// DeleteCredential removes the stored credential for accountID/credType.
DeleteCredential(ctx context.Context, accountID, credType string) error
// HasCredential reports whether any credential exists for accountID/credType.
HasCredential(ctx context.Context, accountID, credType string) (bool, error)
// EncryptPayload encrypts plaintext using the store's AES-256-GCM key.
// Used to encrypt credential data in the account_registrations table.
EncryptPayload(plaintext []byte) (string, error)
// DecryptPayload decrypts a ciphertext blob encrypted by EncryptPayload.
DecryptPayload(ciphertext string) ([]byte, error)
}
CredentialStore handles encrypted credential blobs backed by the account_credentials table. The interface is defined here (not in config/) to avoid circular imports: credentials→config is fine; config→credentials would be circular.
func NewCredentialStore ¶
func NewCredentialStore(pool *pgxpool.Pool, encKey []byte) CredentialStore
NewCredentialStore creates a CredentialStore backed by PostgreSQL. encKey must be the 32-byte AES-256 key (obtain via LoadKey).
type GCPResolveOptions ¶
type GCPResolveOptions struct {
// Signer is the OIDC issuer signer for this CUDly deployment.
Signer oidc.Signer
// IssuerURL is the base URL this deployment publishes OIDC at
// (e.g. "https://<cudly>/oidc"). Must match what the target GCP
// Workload Identity Pool provider's issuer-uri is registered as.
IssuerURL string
}
GCPResolveOptions carries per-deployment wiring that ResolveGCPTokenSource needs to pick the secret-free federated path. A zero value selects the legacy stored-JSON path for backward compatibility with accounts registered before the redesign.
type STSClient ¶
type STSClient interface {
AssumeRole(ctx context.Context, params *sts.AssumeRoleInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleOutput, error)
AssumeRoleWithWebIdentity(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, optFns ...func(*sts.Options)) (*sts.AssumeRoleWithWebIdentityOutput, error)
}
STSClient is the STS interface required for role assumption and web identity federation. Satisfied by *sts.Client from github.com/aws/aws-sdk-go-v2/service/sts.
type STSClientFactory ¶
type STSClientFactory func(provider aws.CredentialsProvider) STSClient
STSClientFactory builds an STSClient from a credentials provider. Used by the bastion path to construct an STS client backed by the bastion account's own credentials before assuming the target role.