Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 47 additions & 5 deletions docs/PERMISSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,47 @@ that close the escalation paths. Replace `ACCOUNT_ID` with your account number b
Together these ensure that even though `iam:CreateRole` targets `Resource: "*"`, every created role is capped by the
boundary, and neither the boundary nor its attachment can be tampered with.

> **Note:** These deny statements require a corresponding update to the AgentCore CDK constructs. The constructs do not
> currently attach a `permissionsBoundary` to the IAM roles they create (runtime, memory, gateway, etc.), so
> CloudFormation will fail to create those roles when `ForceExecutionRoleBoundary` is active. Until the CDK constructs
> are updated to accept and apply a permission boundary ARN, treat this section as a recommended future configuration.
> You can still create the boundary policy (Step 1) and scope the user policy (Step 3) today.
> **Required:** `ForceExecutionRoleBoundary` denies `iam:CreateRole` unless the new role carries the boundary, so you
> must also declare the boundary in your project — otherwise `agentcore deploy` fails while CloudFormation creates the
> agent runtime execution role. See [Step 2b](#step-2b-declare-the-boundary-in-your-project).

### Step 2b: Declare the boundary so the CLI applies it

A boundary is a property of the account you deploy into, so the usual place for it is your machine's global config — set
it once and every project on that machine picks it up:

```bash
agentcore config permissionsBoundary AgentCoreExecutionRoleBoundary
```

If instead your whole team deploys into the same boundary-enforcing account and you want the constraint reviewed and
reproducible in CI, commit it to `agentcore/agentcore.json`:

```json
{
"name": "MyProject",
"version": 1,
"iam": {
"permissionsBoundary": "AgentCoreExecutionRoleBoundary"
}
}
```

Either way the CLI applies it to every IAM role the project creates — the agent runtime execution role plus memory,
gateway, harness, payment and A/B test roles. A bare policy name is resolved against each deployment target's own
partition and account, so a single value works across accounts and partitions. A full policy ARN
(`arn:aws:iam::111122223333:policy/AgentCoreExecutionRoleBoundary`) is used verbatim.

Sources are consulted most-specific first, so a project value overrides the machine default, and
`AGENTCORE_PERMISSIONS_BOUNDARY` overrides both — useful in CI, or for an account whose boundary differs from the one
committed to the project:

```bash
AGENTCORE_PERMISSIONS_BOUNDARY=arn:aws:iam::111122223333:policy/AgentCoreExecutionRoleBoundary agentcore deploy
```

Confirm it landed before deploying, with `agentcore deploy --diff` or by inspecting the synthesized template — every
`AWS::IAM::Role` should carry a `PermissionsBoundary` property.

### Step 3: Scope the user policy to your account

Expand Down Expand Up @@ -303,6 +339,12 @@ policy.
for the token vault. The developer policy needs `kms:CreateKey` and `kms:TagResource`. If your organization restricts
KMS key creation, have an admin pre-create the key and configure it via the token vault settings.

**Deploy fails with `not authorized to perform: iam:CreateRole ... with an explicit deny in a permissions boundary`.**
Your account requires every new role to carry a permissions boundary. The CLI detects this specific failure and prints
the boundary the account expects along with the command to set it, so following that hint and redeploying is usually
enough. See [Step 2b](#step-2b-declare-the-boundary-so-the-cli-applies-it). Raw CloudFormation reports this as
`UnauthorizedTaggingOperation` because the denied `CreateRole` call also carries tags; the boundary is the actual cause.

---

# Permissions Reference
Expand Down
77 changes: 62 additions & 15 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,72 @@ Main project configuration using a **flat resource model**. Agents, memories, an

### Project Fields

| Field | Required | Description |
| ------------------- | -------- | ----------------------------------------------------------- |
| `name` | Yes | Project name (1-23 chars, alphanumeric, starts with letter) |
| `version` | Yes | Schema version (integer, currently `1`) |
| `tags` | No | Project-level tags applied to all resources |
| `runtimes` | Yes | Array of agent specifications |
| `memories` | Yes | Array of memory resources |
| `credentials` | Yes | Array of credential providers (API key or OAuth) |
| `evaluators` | Yes | Array of custom evaluator definitions |
| `onlineEvalConfigs` | Yes | Array of online eval configurations |
| `payments` | No | Array of payment manager configurations |
| `policyEngines` | No | Array of policy engine configurations |
| `agentCoreGateways` | No | Array of gateway definitions |
| `mcpRuntimeTools` | No | Array of MCP runtime tool definitions |
| `unassignedTargets` | No | Targets not yet assigned to a gateway |
| Field | Required | Description |
| ------------------- | -------- | ------------------------------------------------------------ |
| `name` | Yes | Project name (1-23 chars, alphanumeric, starts with letter) |
| `version` | Yes | Schema version (integer, currently `1`) |
| `tags` | No | Project-level tags applied to all resources |
| `iam` | No | Project-wide IAM settings. See [IAM Settings](#iam-settings) |
| `runtimes` | Yes | Array of agent specifications |
| `memories` | Yes | Array of memory resources |
| `credentials` | Yes | Array of credential providers (API key or OAuth) |
| `evaluators` | Yes | Array of custom evaluator definitions |
| `onlineEvalConfigs` | Yes | Array of online eval configurations |
| `payments` | No | Array of payment manager configurations |
| `policyEngines` | No | Array of policy engine configurations |
| `agentCoreGateways` | No | Array of gateway definitions |
| `mcpRuntimeTools` | No | Array of MCP runtime tool definitions |
| `unassignedTargets` | No | Targets not yet assigned to a gateway |

> Gateway configuration is in the `agentCoreGateways` field. See [Gateways and MCP Tools](#gateways-and-mcp-tools)
> below.

### IAM Settings

```json
{
"iam": {
"permissionsBoundary": "AgentCoreExecutionRoleBoundary"
}
}
```

| Field | Required | Description |
| --------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `permissionsBoundary` | No | IAM policy name or policy ARN attached as the permissions boundary of every project role |

`permissionsBoundary` is applied to every IAM role the project creates — agent runtime execution roles, memory, gateway,
harness, payment and A/B test roles included. Set it when your account denies `iam:CreateRole` unless the new role
carries a boundary, which otherwise fails `agentcore deploy` with an `explicit deny in a permissions boundary` error.

A bare policy name is resolved against each deployment target's own partition and account, so one value works across
targets and partitions. A full ARN is used verbatim.

Because a boundary is a property of the account rather than of the project, it can also be set per machine, which is
usually the better default — it applies to every project and is not committed:

```bash
agentcore config permissionsBoundary AgentCoreExecutionRoleBoundary
```

Sources are consulted most-specific first: `AGENTCORE_PERMISSIONS_BOUNDARY`, then `iam.permissionsBoundary` here, then
the machine's global config. Put it in `agentcore.json` when the whole team deploys into the same boundary-enforcing
account and you want the constraint reviewed in git; use the global config otherwise.

To clear the machine default, set it to an empty string — a blank value counts as unset, and `agentcore config` can only
write keys, not remove them:

```bash
agentcore config permissionsBoundary ''
```

In `agentcore.json`, omit the `iam.permissionsBoundary` key instead; a blank value there is a validation error.

> The `iam` block holds constraints imposed on the project from outside — things the account requires of any role. It is
> not a place to author roles; per-resource `executionRoleArn` remains the way to supply a role you manage yourself.

See [Permissions](./PERMISSIONS.md#hardening-with-permission-boundaries) for the boundary policy itself.

---

## Tags
Expand Down
10 changes: 10 additions & 0 deletions schemas/agentcore.schema.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@
"pattern": "^[\\p{L}\\p{N}\\s_.:/=+\\-@]*$"
}
},
"iam": {
"type": "object",
"properties": {
"permissionsBoundary": {
"type": "string",
"pattern": "^(?:arn:[^:]+:iam::[^:]*:policy\\/.+|[\\w+=,.@-]{1,128})$"
}
},
"additionalProperties": false
},
"runtimes": {
"default": [],
"type": "array",
Expand Down
144 changes: 144 additions & 0 deletions src/cli/aws/__tests__/permissions-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { CDK_PERMISSIONS_BOUNDARY_CONTEXT_KEY, PERMISSIONS_BOUNDARY_ENV_VAR } from '../../constants';
import {
isPermissionsBoundaryArn,
permissionsBoundaryCdkContext,
resolvePermissionsBoundary,
toPermissionsBoundaryArn,
} from '../permissions-boundary';
import { describe, expect, it } from 'vitest';

const BOUNDARY_NAME = 'AgentCoreExecutionRoleBoundary';
const BOUNDARY_ARN = `arn:aws:iam::111122223333:policy/${BOUNDARY_NAME}`;

describe('resolvePermissionsBoundary', () => {
it('returns undefined when nothing is configured', () => {
expect(resolvePermissionsBoundary({ env: {} })).toBeUndefined();
});

it('prefers the explicit override over the environment and config', () => {
const resolved = resolvePermissionsBoundary({
override: 'FromFlag',
configured: 'FromConfig',
env: { [PERMISSIONS_BOUNDARY_ENV_VAR]: 'FromEnv' },
});

expect(resolved).toBe('FromFlag');
});

it('prefers the environment over the project config', () => {
const resolved = resolvePermissionsBoundary({
configured: 'FromConfig',
env: { [PERMISSIONS_BOUNDARY_ENV_VAR]: 'FromEnv' },
});

expect(resolved).toBe('FromEnv');
});

it('falls back to the project config', () => {
expect(resolvePermissionsBoundary({ configured: BOUNDARY_NAME, env: {} })).toBe(BOUNDARY_NAME);
});

it('prefers the project config over the machine global config', () => {
expect(resolvePermissionsBoundary({ configured: 'FromProject', global: 'FromGlobal', env: {} })).toBe(
'FromProject'
);
});

it('falls back to the machine global config', () => {
expect(resolvePermissionsBoundary({ global: BOUNDARY_NAME, env: {} })).toBe(BOUNDARY_NAME);
});

it('trims values and skips blank ones', () => {
const resolved = resolvePermissionsBoundary({
configured: ` ${BOUNDARY_NAME} `,
env: { [PERMISSIONS_BOUNDARY_ENV_VAR]: ' ' },
});

expect(resolved).toBe(BOUNDARY_NAME);
});

// `agentcore config` can only write keys, so `agentcore config permissionsBoundary ''` is the
// only way to clear the machine default. A blank must therefore read as unset, not as a
// fall-through to the next source.
it('treats a blank value as unset at every source', () => {
expect(resolvePermissionsBoundary({ global: '', env: {} })).toBeUndefined();
expect(resolvePermissionsBoundary({ global: ' ', env: {} })).toBeUndefined();
expect(resolvePermissionsBoundary({ configured: '', env: {} })).toBeUndefined();
expect(resolvePermissionsBoundary({ override: '', env: {} })).toBeUndefined();
expect(resolvePermissionsBoundary({ env: { [PERMISSIONS_BOUNDARY_ENV_VAR]: '' } })).toBeUndefined();
});

it('does not let a blank higher-precedence source mask a lower one', () => {
expect(resolvePermissionsBoundary({ override: '', configured: BOUNDARY_NAME, env: {} })).toBe(BOUNDARY_NAME);
expect(resolvePermissionsBoundary({ configured: ' ', global: BOUNDARY_NAME, env: {} })).toBe(BOUNDARY_NAME);
});

it('resolves the full precedence chain in order', () => {
const all = {
override: 'FromOverride',
configured: 'FromProject',
global: 'FromGlobal',
env: { [PERMISSIONS_BOUNDARY_ENV_VAR]: 'FromEnv' },
};

expect(resolvePermissionsBoundary(all)).toBe('FromOverride');
expect(resolvePermissionsBoundary({ ...all, override: undefined })).toBe('FromEnv');
expect(resolvePermissionsBoundary({ ...all, override: undefined, env: {} })).toBe('FromProject');
expect(resolvePermissionsBoundary({ ...all, override: undefined, env: {}, configured: undefined })).toBe(
'FromGlobal'
);
});
});

describe('isPermissionsBoundaryArn', () => {
it('recognises ARNs across partitions', () => {
expect(isPermissionsBoundaryArn(BOUNDARY_ARN)).toBe(true);
expect(isPermissionsBoundaryArn('arn:aws-cn:iam::111122223333:policy/Boundary')).toBe(true);
expect(isPermissionsBoundaryArn('arn:aws-us-gov:iam::111122223333:policy/Boundary')).toBe(true);
});

it('treats bare policy names as names', () => {
expect(isPermissionsBoundaryArn(BOUNDARY_NAME)).toBe(false);
expect(isPermissionsBoundaryArn('arnold-boundary')).toBe(false);
});
});

describe('permissionsBoundaryCdkContext', () => {
it('maps a policy name to the CDK name form', () => {
expect(permissionsBoundaryCdkContext(BOUNDARY_NAME)).toEqual({
[CDK_PERMISSIONS_BOUNDARY_CONTEXT_KEY]: { name: BOUNDARY_NAME },
});
});

it('maps a policy ARN to the CDK arn form', () => {
expect(permissionsBoundaryCdkContext(BOUNDARY_ARN)).toEqual({
[CDK_PERMISSIONS_BOUNDARY_CONTEXT_KEY]: { arn: BOUNDARY_ARN },
});
});
});

describe('toPermissionsBoundaryArn', () => {
it('expands a policy name using the target partition and account', () => {
expect(toPermissionsBoundaryArn(BOUNDARY_NAME, { region: 'us-east-1', accountId: '111122223333' })).toBe(
BOUNDARY_ARN
);
});

it('uses the China partition for cn regions', () => {
expect(toPermissionsBoundaryArn(BOUNDARY_NAME, { region: 'cn-north-1', accountId: '111122223333' })).toBe(
`arn:aws-cn:iam::111122223333:policy/${BOUNDARY_NAME}`
);
});

it('uses the GovCloud partition for us-gov regions', () => {
expect(toPermissionsBoundaryArn(BOUNDARY_NAME, { region: 'us-gov-west-1', accountId: '111122223333' })).toBe(
`arn:aws-us-gov:iam::111122223333:policy/${BOUNDARY_NAME}`
);
});

it('passes an ARN through unchanged', () => {
expect(toPermissionsBoundaryArn(BOUNDARY_ARN, { region: 'us-east-1', accountId: '999988887777' })).toBe(
BOUNDARY_ARN
);
});
});
9 changes: 9 additions & 0 deletions src/cli/aws/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
export { detectAwsContext, type AwsContext } from './aws-context';
export { detectAccount, getCredentialProvider } from './account';
export { getPartition, arnPrefix, dnsSuffix, serviceEndpoint, consoleDomain } from './partition';
export {
resolvePermissionsBoundary,
isPermissionsBoundaryArn,
permissionsBoundaryCdkContext,
toPermissionsBoundaryArn,
type PermissionsBoundaryContextValue,
type ResolvePermissionsBoundaryOptions,
type PermissionsBoundaryArnContext,
} from './permissions-boundary';
export { detectRegion, type RegionDetectionResult } from './region';
export { applyTargetRegionToEnv, withTargetRegion } from './target-region';
export {
Expand Down
Loading
Loading