Skip to content

Keyless Credentials (S3 AssumeRole & RDS IAM)

Clonit’s default credential models store a static secret: storage profiles hold encrypted S3/R2 access keys, and targets hold a database password (see Database Credentials for the full spectrum of password handling, including the cloud sync secret modes). Two keyless modes remove the stored secret entirely.

Local storage profiles (the ones this machine pushes and pulls with directly) can also go keyless: instead of stored keys, a profile can ride the agent’s own AWS credential chain — see Local storage profiles below.

The single most important thing: the two keyless modes use two different AWS principals. Keyless storage uses clonit-cloud’s identity; keyless database auth uses the agent’s identity. A customer told “one role covers both” will mis-scope their setup. See Two principals below.

Keyless storage (S3 cross-account AssumeRole)

Section titled “Keyless storage (S3 cross-account AssumeRole)”

A storage profile gains an auth_mode: static (today — encrypted access/secret keys) or assume_role (S3 only). With assume_role the cloud stores a role ARN and a server-minted ExternalIdno keys. At presign time the cloud broker sts:AssumeRoles into your role and signs the URL with the resulting temporary, session-bearing credentials.

auth_mode=assume_role → cloud stores: role_arn + (secret) ExternalId, NO keys
presign: broker sts:AssumeRole → temp creds → presigned URL
  1. Create the profile in keyless mode. From the CLI:

    Terminal window
    clonit cloud storage-profiles add \
    --name team-keyless \
    --provider s3 \
    --bucket my-snapshots \
    --region us-east-1 \
    --auth-mode assume_role \
    --role-arn arn:aws:iam::123456789012:role/clonit-access

    Or from the web UI (clonit serve): Storage Profiles → Add Cloud Profile, then set Credentials to Keyless — AssumeRole (S3 only) and fill in the Role ARN. Either way, the cloud server-mints a unique ExternalId, encrypts it at rest, and never returns it in any ordinary API response.

  2. Verify — run clonit cloud storage-profiles verify team-keyless. It performs a throwaway sts:AssumeRole to report whether the grant works, and prints the onboarding artifact: the cloud’s principal ARN, your ExternalId (surfaced only by verify), and a copy-pasteable trust policy and permission policy with both values interpolated. In the web UI the onboarding dialog opens automatically after saving a keyless profile, runs Verify for you, and shows the same artifact with copy buttons. The first verify is expected to fail — your role doesn’t trust the cloud yet.

  3. Attach the policies to your role, then re-run verify (CLI) or click Re-verify (UI):

// Trust policy (who may assume the role)
{ "Effect": "Allow",
"Principal": { "AWS": "<CLONIT_CLOUD_PRINCIPAL_ARN>" },
"Action": "sts:AssumeRole",
"Condition": { "StringEquals": { "sts:ExternalId": "<EXTERNAL_ID>" } } }
// Permission policy (what the role may do) — bucket/prefix-scoped
// s3:GetObject, s3:PutObject, s3:DeleteObject on arn:aws:s3:::<bucket>/<prefix>/*
// (s3:DeleteObject is used by the cloud retention sweep; roles created before
// it was part of onboarding degrade gracefully — deletes are audited + skipped
// until you add the grant)

The CLI and UI sit on top of the same REST surface — POST /storage-profiles/{id}/verify runs the check and returns the artifact if you are scripting against the API directly.

Cloudflare R2 has no cross-account role grant. The recommended R2 posture is a bucket-scoped, revocable R2 API token used AS your static access/secret (not account Super-Admin keys). There is no assume_role for R2; auth_mode is exactly {static, assume_role}.

clonit cloud storage-profiles update <name> (or the web UI’s edit form) changes auth_mode/role_arn and rotates keys or the ExternalId in place — the profile id stays stable so snapshots that reference it are never orphaned. Switching to assume_role clears any stored keys and mints a new ExternalId (re-run verify and re-attach the trust policy); switching back to static clears the role ARN and ExternalId, and keys must be re-supplied. The underlying API is PUT /storage-profiles/{id}.

assume_role requires clonit-cloud to run under a stable, non-assumed IAM identity (set aws.region and, ideally, aws.principal_arn so the published ARN is frozen). Run the broker directly under that identity — if the broker is itself an assumed role, STS role-chaining caps the session at 1h, and any presign_expiry at/above that fails loudly.

The broker requests an assumed session of presign_expiry + 15m (so the signing credentials outlive every presigned URL and the cached session can refresh before it ages out). The customer role’s MaxSessionDuration must therefore exceed presign_expiry — for the default 1h presign_expiry, raise the customer role’s MaxSessionDuration above 1h (the AWS default is exactly 1h). If the session a role returns is shorter than presign_expiry, the presign fails loudly rather than mint a URL that 403s after the session expires — lower presign_expiry or raise the role’s MaxSessionDuration.

Local storage profiles: three credential sources

Section titled “Local storage profiles: three credential sources”

The AssumeRole mode above covers cloud (org-level) storage profiles. Local storage profiles — the ones this machine uses directly for push / pull — have their own credential choice. The storage profile form’s Authentication section offers a Credential Source selector:

Credential Source What is stored Best for
Access keys (stored encrypted) Access/secret key, encrypted at rest Providers that issue their own tokens (R2), or machines with no AWS identity
Shared AWS profile (~/.aws) Only the profile name Machines with ~/.aws config, including SSO-backed profiles
Agent credential chain (keyless) Nothing AWS environments — instance/task roles, environment credentials, SSO

With the agent credential chain, the profile stores no credential at all: at push/pull time the agent resolves whatever AWS identity its environment already provides — the same identity, resolved in the same order, as keyless database auth below. Check Settings → AWS to see which identity that is and whether it authenticates.

Provider caveats:

  • MinIO needs keys or a shared profile — the keyless option is not offered (a self-hosted gateway has no ambient AWS identity to resolve against).
  • R2 usually needs its own bucket-scoped API tokens as stored keys (see R2 has no keyless mode).
  • Azure always uses stored account keys.
  • GCS goes keyless its own way: leave the service-account JSON empty and it falls back to Application Default Credentials.

Organizations that never issue static keys at all can pair keyless local profiles (agent side) with the cloud’s AssumeRole mode (broker side): role-based with an ExternalId, no keys stored in either place.

A target gains a db_auth_mode: password (the default password handling) or rds_iam (RDS / Aurora — PostgreSQL/MySQL/MariaDB). With rds_iam the target stores no password. The agent mints a short-lived (~15-min) SigV4 token locally and uses it as the DB password over verified TLS.

db_auth_mode=rds_iam → stored secret: NONE
agent mints rds-db:connect token per connection (fresh each time)

How keyless database access works (plain English)

Section titled “How keyless database access works (plain English)”

Instead of a stored password, the agent asks AWS to sign a one-time pass: a token valid for about 15 minutes, for one database user on one database server. RDS checks the signature and lets the connection in. The token is minted fresh for every connection and never written anywhere — not to disk, not to the config, not to the cloud.

Three things make it work:

  1. The grant — the database user is flagged for IAM authentication (one SQL statement, shown below and in the guided setup wizard).
  2. The permission — the agent’s AWS identity is allowed to request passes for that user, via a narrowly scoped rds-db:connect policy.
  3. The certificate bundle — the connection is encrypted and verified against the real server, so a pass can’t be captured in transit.

Where does the agent’s AWS identity come from? Wherever the environment already has one, checked in order: environment variables, a shared AWS profile (including SSO), a container/task role, an EC2 instance role. Clonit stores none of these — it borrows whatever identity the environment already has. The Settings → AWS page (its AWS credentials card) and the guided setup wizard both show which identity that currently is.

Why security teams tend to be OK with it:

  • There is nothing long-lived to leak or rotate — no password, no stored key.
  • Passes expire in minutes, so a captured token has almost no shelf life.
  • Permission is scoped to one database user on one server, not an account-wide grant.
  • Access is revoked centrally in IAM — detach the policy and no more passes are issued.
  • Connections refuse to run over unverified TLS, so a pass is never sent to an impostor server.

Choosing which AWS identity the agent uses

Section titled “Choosing which AWS identity the agent uses”

Most setups never pick a profile — the agent just borrows whatever identity the environment provides. When you do need control (say, a laptop with several ~/.aws profiles, or one target that must sign with a different account), the agent resolves the identity for keyless database auth in this order, first match wins:

  1. The target’s AWS Profile field — set in the target form (keyless database mode) or with --aws-profile on targets add / targets update. Per-target, beats everything else.
  2. The agent config aws.profile — an agent-wide shared profile (see Configuration).
  3. The environmentAWS_PROFILE or environment access keys, including values from a gitignored .env.local file, which the agent loads into its environment at startup.
  4. The shared-config default profile (~/.aws).
  5. An instance or task role (EC2/ECS).

Keyless storage mirrors the same ladder from the storage profile’s side: the profile’s stored access keys > the profile’s AWS CLI profile > the agent config aws.profile > the ambient chain (steps 3–5 above).

Settings → AWS is where you view and set the agent-wide profile: the input previews what any named ~/.aws profile resolves to, and Save stores it as the agent’s aws.profile — effective immediately, no restart. New keyless targets start from the saved value (each target keeps its own copy you can edit or clear), while the guided setup wizard shows the profile in effect for the specific target you are setting up.

Concern Posture
No long-lived secrets No database password is stored — anywhere. Each connection uses a freshly minted, ~15-minute token.
No static AWS keys required In AWS, the agent uses its instance or task role. Elsewhere, SSO or a shared AWS profile works — clonit itself never requires access keys.
Key rotation A non-event: clonit stores nothing, so there is nothing to rotate on the clonit side.
Least privilege The policy the guided setup produces is scoped to a single database user on a single instance.
No write access needed to your AWS account The guided setup only reads (describe calls) to fill in details; every actual change is applied by your own admins.
Transport security TLS is fail-closed with a pinned certificate bundle — no bundle, no connection, never a silent downgrade.

Set the auth mode at create or update time. --db-region is optional — it is derived from an RDS/Aurora hostname when omitted, and only needed for non-RDS hostnames or custom DNS:

Terminal window
clonit target add --name prod \
--src-url 'postgres://appuser@db.abc123.us-east-1.rds.amazonaws.com:5432/app' \
--db-auth-mode rds_iam # --db-region us-east-1 (optional)
clonit target update prod --db-auth-mode rds_iam # migrate an existing target

The src-url/dst-url carry no password for rds_iam (the agent mints the token as the password at connect time); any password present is ignored.

  • PostgreSQL: GRANT rds_iam TO <dbuser>;
  • MySQL/MariaDB: CREATE USER '<dbuser>'@'%' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'; (for an existing user, ALTER USER '<dbuser>'@'%' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS';)
  • Destination users (destination / sanitize destination URLs) also need create-database rights — clonit drops and recreates those databases on every load, so they don’t need to exist up front. PostgreSQL: ALTER ROLE <dbuser> CREATEDB; (or CREATE USER <dbuser> CREATEDB;). MySQL/MariaDB: GRANT ALL ON <db>.* TO '<dbuser>'@'%'; works even before the schema exists.
  • IAM policy on the agent’s role: rds-db:connect on the DbiResourceId ARN (arn:aws:rds-db:<region>:<acct>:dbuser:<DbiResourceId>/<dbuser>) — the db-XXXX/cluster resource id, not the human DB name.

rds_iam works in AWS (instance/task role → zero stored secrets) and outside AWS (env keys / shared profile to sign the token — still no DB password stored). If no AWS credential chain is found, the agent fails with a distinct, actionable error (not a confusing blank-password DB failure).

A bearer token over an unverified TLS session is a credential-capture path, so rds_iam defaults to certificate-verifying TLS: sslmode=verify-full (PG) / --ssl-mode=VERIFY_IDENTITY (MySQL) with the RDS CA bundle. The setup wizard’s CA-bundle step downloads the AWS global bundle to <config dir>/rds-global-bundle.pem with one click (takes effect immediately, no restart), or set rds_ca_bundle_path to a bundle you provision yourself. With no bundle on disk, the connection fails closed rather than downgrade — clonit will not send a token over unverified TLS.

Honest fine print:

  • Some AWS identity must exist where the agent runs. Keyless doesn’t mean identity-free — in AWS this is the instance/task role; on laptops, prefer an SSO session over long-lived access keys.
  • Where logins are audited: IAM database logins appear in the database engine logs, not as CloudTrail API events. (The guided setup’s lookup calls are CloudTrail-visible.)
  • Connection-rate cap: AWS limits IAM-auth connections to roughly 200 per second per instance — irrelevant for snapshot workloads, which open a handful of connections per run.
  • Static keys and rotation: if the agent runs with static environment keys, rotating them requires an agent restart to pick up the new values; roles and SSO sessions refresh automatically.
Storage (S3 AssumeRole) Database (RDS IAM)
Whose AWS identity clonit-cloud’s stable principal the agent’s identity
Where it runs the cloud broker the agent (clonit)
Customer grants a role trusting the cloud principal + ExternalId rds-db:connect on the agent’s role to the DbiResourceId
clonit stores role ARN + (secret) ExternalId; NO keys NOTHING

The agent never holds the assumed storage role (storage brokering runs only in the cloud), and the cloud never touches your databases (database auth runs only in the agent). The “run under one IAM role, store no secrets” pitch only applies to a standalone, self-brokering agent (which does its own S3 and its own RDS under one instance role).