Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
2a8babe
feat(secrets): add sensitive-key registry for at-rest encryption
aidandaly24 Jun 23, 2026
52a95e6
feat(errors): add SecretEncryptionError and SecretDecryptionError
aidandaly24 Jun 23, 2026
f6cb1d2
feat(secrets): add AES-256-GCM cipher for at-rest secret envelopes
aidandaly24 Jun 23, 2026
dd410f0
feat(secrets): add key-provider chain (OS keychain -> keyfile fallback)
aidandaly24 Jun 23, 2026
0df7129
refactor(secrets): extract resolveConfigDir helper in key-provider
aidandaly24 Jun 23, 2026
49f7938
feat(secrets): encrypt sensitive .env.local values at rest in env.ts
aidandaly24 Jun 23, 2026
9a0426e
fix(payment-connector): warn when secrets are passed as literal CLI f…
aidandaly24 Jun 23, 2026
edb0307
chore(secrets): add optional @napi-rs/keyring + document at-rest encr…
aidandaly24 Jun 23, 2026
e9b66f3
fix(build): mark @napi-rs/keyring external so esbuild does not bundle…
aidandaly24 Jun 23, 2026
5d42e0e
test(secrets): assert model-provider keys are encrypted at rest in .e…
aidandaly24 Jun 23, 2026
2f53e08
test(secrets): assert OAuth client secret is encrypted at rest in add…
aidandaly24 Jun 23, 2026
9891bb7
fix(secrets): decrypt with all candidate keys, fix warning placement …
aidandaly24 Jun 26, 2026
6f76853
fix(secrets): reject credential names that would store the secret une…
aidandaly24 Jun 26, 2026
62ab702
fix: harden machine key handling and centralize the literal-secret-fl…
aidandaly24 Jun 26, 2026
2b5c461
test(e2e): assert payment connector secrets are encrypted at rest bef…
aidandaly24 Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions docs/payments.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,28 @@ AGENTCORE_CREDENTIAL_{CREDENTIAL_NAME}_AUTHORIZATION_ID=...
`{CREDENTIAL_NAME}` is the connector's credential name uppercased with hyphens replaced by underscores. For example, a
credential named `my-cdp-creds` becomes `AGENTCORE_CREDENTIAL_MY_CDP_CREDS_API_KEY_ID`.

### Secret encryption at rest

Connector secret values in `agentcore/.env.local` (API key secrets, wallet secrets, private keys) are encrypted at rest
with a machine-local key stored outside the project directory (your OS keychain, or `~/.agentcore/secrets.key` on
machines without a keychain). Copying or syncing the project directory does **not** expose the secrets — only the holder
of the machine key can decrypt them. Reference IDs (API key ID, app ID) remain readable.

To rotate a credential, re-run `agentcore add payment-connector` with the new values (this re-encrypts immediately).
Editing `agentcore/.env.local` by hand still works: paste the new plaintext value and run `agentcore deploy` — the CLI
re-encrypts it on the next write.

### Credential Rotation

To rotate credentials:
The preferred rotation path is to re-run `agentcore add payment-connector` with the new values — the CLI re-encrypts the
secrets immediately and there is no plaintext window. Hand-editing `.env.local` also works: paste the new plaintext
value and run `agentcore deploy -y` — the CLI re-encrypts on the next write.

1. Update the values in `agentcore/.env.local`
1. Re-run `agentcore add payment-connector` with the new values (preferred), **or** open `agentcore/.env.local`, paste
the new plaintext secret value, and save.
2. Run `agentcore deploy -y`

Deploy automatically updates the PaymentCredentialProvider on AWS with the new secret values.
Deploy automatically updates the PaymentCredentialProvider on AWS with the new (re-encrypted) secret values.

## Deploying with Payments

Expand Down
15 changes: 15 additions & 0 deletions e2e-tests/payment-strands-bedrock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ describe.sequential('e2e: payments — create → add payment → deploy → sta
expect(cred).toBeTruthy();
});

it.skipIf(!canRun)('stores connector secrets encrypted at rest, never as plaintext', async () => {
// The deploy step below reads these same values back and decrypts them, so a
// passing deploy already proves the round trip works. This assertion additionally
// pins the security invariant the encryption feature exists to guarantee: the raw
// secret must never touch disk in cleartext. Without it, a regression that silently
// reverted to plaintext storage would still deploy green and go unnoticed.
const envLocal = await readFile(join(projectPath, 'agentcore', '.env.local'), 'utf-8');

expect(envLocal, '.env.local should contain at least one enc:v1: envelope').toContain('enc:v1:');

for (const raw of [process.env.CDP_API_KEY_SECRET!, process.env.CDP_WALLET_SECRET!]) {
expect(envLocal, 'raw secret value must not appear in cleartext on disk').not.toContain(raw);
}
});

it.skipIf(!canRun)('has payment capability code in agent', async () => {
const config = JSON.parse(await readFile(join(projectPath, 'agentcore', 'agentcore.json'), 'utf-8'));
const runtimeName = config.runtimes?.[0]?.name;
Expand Down
2 changes: 1 addition & 1 deletion esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ await esbuild.build({
banner: {
js: `import { createRequire } from 'module'; import { fileURLToPath as __ef } from 'url'; import { dirname as __ed } from 'path'; const require = createRequire(import.meta.url); const __filename = __ef(import.meta.url); const __dirname = __ed(__filename);`,
},
external: ['fsevents', '@aws-cdk/toolkit-lib'],
external: ['fsevents', '@aws-cdk/toolkit-lib', '@napi-rs/keyring'],
plugins: [optionalDepsPlugin, textLoaderPlugin],
});

Expand Down
20 changes: 18 additions & 2 deletions integ-tests/add-agent-auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
import { readEnvFile } from '../src/lib/utils/env.js';
import { createTestProject, readProjectConfig, runCLI } from '../src/test-utils/index.js';
import type { TestProject } from '../src/test-utils/index.js';
import { mkdtempSync, rmSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

describe('integration: add BYO agent with CUSTOM_JWT auth', () => {
let project: TestProject;
let keyDir: string;
const prevEnv = { kc: process.env.AGENTCORE_DISABLE_KEYCHAIN, cfg: process.env.AGENTCORE_CONFIG_DIR };
const agentName = 'AuthAgent';
const discoveryUrl = 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_test123/.well-known/openid-configuration';

beforeAll(async () => {
// Isolate secret-at-rest encryption from the developer's real OS keychain and
// ~/.agentcore: force the keyfile provider into a throwaway dir. cleanSpawnEnv
// inherits process.env, so spawned CLI processes pick these up too.
keyDir = mkdtempSync(join(tmpdir(), 'agentcore-integ-keys-'));
process.env.AGENTCORE_DISABLE_KEYCHAIN = '1';
process.env.AGENTCORE_CONFIG_DIR = keyDir;
project = await createTestProject({
noAgent: true,
});
});

afterAll(async () => {
await project.cleanup();
process.env.AGENTCORE_DISABLE_KEYCHAIN = prevEnv.kc;
process.env.AGENTCORE_CONFIG_DIR = prevEnv.cfg;
rmSync(keyDir, { recursive: true, force: true });
});

it('adds a BYO agent with CUSTOM_JWT authorizer and audience', async () => {
Expand Down Expand Up @@ -119,11 +133,13 @@ describe('integration: add BYO agent with CUSTOM_JWT auth', () => {
expect(oauthCred!.authorizerType).toBe('OAuthCredentialProvider');
expect((oauthCred as { managed?: boolean }).managed).toBe(true);

// Verify .env.local has client secrets (namespaced per credential)
// Client ID is a reference (plaintext); the client SECRET is encrypted at rest.
const envPath = join(project.projectPath, 'agentcore', '.env.local');
const envContent = await readFile(envPath, 'utf-8');
expect(envContent).toContain('my-client-id');
expect(envContent).toContain('my-client-secret');
expect(envContent).not.toContain('my-client-secret'); // encrypted at rest
const env = await readEnvFile(join(project.projectPath, 'agentcore'));
expect(env.AGENTCORE_CREDENTIAL_AUTHAGENT2_OAUTH_CLIENT_SECRET).toBe('my-client-secret');
});

it('adds a BYO agent with default AWS_IAM auth (no auth flags)', async () => {
Expand Down
223 changes: 223 additions & 0 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
"yaml": "^2.8.3",
"zod": "^4.3.5"
},
"optionalDependencies": {
"@napi-rs/keyring": "^1.3.0"
},
"peerDependencies": {
"aws-cdk-lib": "^2.258.0",
"constructs": "^10.0.0"
Expand Down
Loading
Loading