ADR 0008: Keyless AWS credentials, aws_role backends, and bootstrapped gateway keys
- Status: Accepted
- Date: 2026-09-06
Context
The gateway historically required static long-lived credentials for every S3
destination: S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY for the global
single-tenant client, and a persisted s3_compatible access/secret pair for
per-workspace backends. BackendType::AwsRole existed in the schema but was
rejected at configuration time. Static keys are a security liability and are
awkward to rotate, and AWS-native deployments should be able to rely on
short-lived, automatically-refreshed credentials instead.
At the same time, headless automation had no stable gateway credential: the only key material was either generated at startup (demo mode) or minted interactively, so operators had to copy a freshly generated secret on every cold start.
Decision
-
Default credential provider chain for the global client. When
S3_ENDPOINTis configured without staticS3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY, the global single-tenant client is built without an explicitcredentials_provider, deferring to the AWS default chain (EC2 instance profile, ECS task role, EKS IRSA, SSO, OIDC web identity). Static keys still take precedence when both are present. -
aws_roleper-workspace backends via STS.RuntimeBackendConfig::AwsRolecarries a role ARN, region, and optionalexternal_id. On resolution the gateway assumes the role withaws-sdk-sts(using the default credential chain), then builds the destination client from the returned temporary credentials (access key, secret, session token, expiry). The destination is always the canonical AWS regional endpoint (https://s3.<region>.amazonaws.com), so the operator endpoint allowlist does not apply; the trust boundary is the role ARN itself plus the identity allowed to assume it, reinforced by the optionalexternal_id. -
Bootstrapped gateway key.
MASKURA_BOOTSTRAP_KEY/MASKURA_BOOTSTRAP_SECRET(with permanentS4_*aliases) seed a preconfigured key id/secret pair at startup when the pair is not already present. The secret is SHA-256 hashed and encrypted with the same envelope as generated keys. This is scoped to operator/headless bootstrap; interactive and production flows keep using the normal key-creation path.
Consequences
- No long-lived AWS keys in the keyless path. Credentials resolve lazily via the ambient AWS identity, so rotation is handled by the platform (IRSA, SSO, instance profile) rather than by redistributing secrets.
- STS assume-role happens per request. Temporary credentials are used for a single operation and are never serialized or persisted. This trades a per-request STS call for the absence of any credential cache; a short-TTL in-process cache is a follow-up if request volume justifies it.
aws_roleconfig is validated at the boundary. Role ARNs must bearn:...:role/..., a region is required, and static credentials/endpoints are rejected for this backend type. The redacted dashboard response exposesexternal_id(it is a correlation value, not a secret) but never the temporary credentials.- Schema change.
BackendConfigRequestandBackendConfigResponsegain anexternal_idfield, requiring SDK regeneration viajust build-sdks. - Bootstrapped keys are operator-managed. They are printed nowhere at startup and rely on the operator rotating them out of band; they must not be used in place of per-user keys where attribution matters.