Skip to content

Tutorial: cloud credentials

Goal

At the end you will see the agent run the AWS CLI with a credential that OpenBao creates for that one run and deletes when the command ends, and run gcloud with an access token it never sees.

Every command and output below comes from a real run, recorded in demo/tutorials/cloud/expected/ and checked by run.sh --check.

What is real and what is emulated. OpenBao and its AWS secrets plugin are the real ones. AWS itself is moto, a local emulator of the IAM and STS APIs, so the run needs no AWS account. The Google Cloud plugin needs a real Google project, so the gcloud case keeps a fake token in the KV and shows the file mechanism that gcp: credentials use.

Run it

pip install "moto[server]"     # plus the aws and gcloud CLIs
export KH_AWS_PLUGIN=/path/to/openbao-plugin-secrets-aws   # from the plugin's GitHub release
bash demo/tutorials/cloud/run.sh all
bash demo/tutorials/cloud/run.sh --check

Script: demo/tutorials/cloud/run.sh. The session uses two policies: policy-project-read.hcl and policy-cloud.hcl. KEYHOLDER_DYNAMIC=aws/creds/deploy.

1. The human enables the AWS engine

Outside the chat. The role creates an IAM user per credential, allowed only to call sts:GetCallerIdentity.

# The human enables the AWS secrets engine and a role (outside the chat)

$ bao secrets enable -path=aws openbao-plugin-secrets-aws
Success! Enabled the openbao-plugin-secrets-aws secrets engine at: aws/
exit_code=0

$ bao write aws/config/root access_key=moto-root secret_key=moto-root-secret region=us-east-1 iam_endpoint=http://127.0.0.1:15000 sts_endpoint=http://127.0.0.1:15000
Success! Data written to: aws/config/root
exit_code=0

$ bao write aws/roles/deploy credential_type=iam_user policy_document={"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"sts:GetCallerIdentity","Resource":"*"}]}
Success! Data written to: aws/roles/deploy
exit_code=0

The emulator's root keys are fake. With real AWS they are an IAM user that may create users; the agent never gets them.

2. The agent sees the path, not a credential

keyholder_list shows the allowed dynamic path and how to use it. Listing never reads it: a read would create a credential.

# The agent sees the dynamic path and how to use it, not a credential

$ BAO_WRAPPING_TOKEN=<wrapped token> khcall keyholder_list
{
  "dynamic": [
    {
      "path": "aws/creds/deploy",
      "use": "credentials: [\"aws:aws/creds/deploy\"]"
    }
  ],
  "entries": [
    {
      "ref": "vault://my-project/gcp/ACCESS_TOKEN",
      "version": 1
    }
  ],
  "note": "Use {{vault://path/KEY}} in the args, env or files of keyholder_run. Dynamic paths are read when the command runs and revoked after it; use them through credentials or {{bao://path#field}}.",
  "scope": "my-project"
}
exit_code=0

3. A credential for one run

The agent asks for credentials: ["aws:aws/creds/deploy"]. Keyholder reads the path once, sets AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY for the child, runs aws, and revokes the lease when it exits. The ARN shows the user OpenBao created for this run; after the run no user is left.

# The agent runs the AWS CLI with a credential made for this run

$ BAO_WRAPPING_TOKEN=<wrapped token> khcall keyholder_run '{"command":"aws","args":["--endpoint-url","http://127.0.0.1:15000","sts","get-caller-identity","--query","Arn","--output","text"],"env":{"AWS_DEFAULT_REGION":"us-east-1"},"credentials":["aws:aws/creds/deploy"]}'
{
  "exit_code": 0,
  "notes": [
    "revoked 1 dynamic credential lease(s) after the run"
  ],
  "refs_used": [
    "bao://aws/creds/deploy#access_key",
    "bao://aws/creds/deploy#secret_key"
  ],
  "stderr": "",
  "stdout": "arn:aws:iam::123456789012:user/vault-token-deploy-<id>\n"
}
--- stderr
<timestamp> audit cmd="aws" refs=[bao://aws/creds/deploy#access_key bao://aws/creds/deploy#secret_key] exit=0 dur=<duration> status=ok
exit_code=0

IAM users left in AWS after the run (checked by the human): 0

Keyholder revoked the lease when aws exited, and OpenBao deleted the user it had created for the run.

4. Only listed paths

A path outside KEYHOLDER_DYNAMIC is refused before anything is read, even when the policy would allow it.

# A dynamic path outside KEYHOLDER_DYNAMIC is refused before any read

$ BAO_WRAPPING_TOKEN=<wrapped token> khcall keyholder_run '{"command":"aws","args":["sts","get-caller-identity"],"credentials":["aws:aws/creds/admin"]}'
error: bao://aws/creds/admin#access_key: dynamic path not allowed: it must be listed in KEYHOLDER_DYNAMIC (aws/creds/admin)
--- stderr
<timestamp> audit cmd="aws" refs=[] exit=0 dur=<duration> status=error: bao://aws/creds/admin#access_key: dynamic path not allowed: it must be listed in KEYHOLDER_DYNAMIC (aws/creds/admin)
exit_code=1

5. gcloud with an access token file

files gives gcloud the path of a temporary file holding the token (CLOUDSDK_AUTH_ACCESS_TOKEN_FILE), deleted after the run. gcloud prints the token it was given; the agent reads it masked. With the Google Cloud plugin, credentials: ["gcp:gcp/roleset/<name>/token"] does the same with a token the plugin creates.

# gcloud reads an access token from a file it gets for this run only

$ BAO_WRAPPING_TOKEN=<wrapped token> khcall keyholder_run '{"command":"gcloud","args":["auth","print-access-token"],"env":{"CLOUDSDK_CONFIG":"/tmp/keyholder-tutorial-gcloud","CLOUDSDK_CORE_DISABLE_PROMPTS":"1"},"files":{"CLOUDSDK_AUTH_ACCESS_TOKEN_FILE":"{{vault://my-project/gcp/ACCESS_TOKEN}}"}}'
{
  "exit_code": 0,
  "refs_used": [
    "vault://my-project/gcp/ACCESS_TOKEN"
  ],
  "stderr": "",
  "stdout": "[hidden:vault://my-project/gcp/ACCESS_TOKEN]\n"
}
--- stderr
<timestamp> audit cmd="gcloud" refs=[vault://my-project/gcp/ACCESS_TOKEN] exit=0 dur=<duration> status=ok
exit_code=0

What this shows

  • A dynamic credential exists only for the command: read at execution, revoked after it.
  • The agent sees the path and the result, never the keys or the token.
  • Revocation depends on the credential type: IAM users are deleted; STS credentials and GCP access tokens can only expire. See Configuration → Dynamic credentials.