Skip to content
Merged
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
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ Read [the migration guide](docs/MIGRATION.md), [the Auth0/AWS explanation](docs/

## Usage

Copy `examples/deploy.dev.json` / `examples/deploy.prod.json` into the service repository and replace the example identifiers with administrator-verified values. These are placeholders, not guessed Topcoder targets. Generate IAM documents offline:
Copy `examples/deploy.dev.json` / `examples/deploy.prod.json` into the service repository and replace the example identifiers with administrator-verified values. These are placeholders, not guessed Topcoder targets.

```sh
python3 -I tcdeploy.py policies --manifest deploy/dev.json > policies.json
```
The OIDC provider, deployment roles, trust policies, permissions and boundaries must already be installed through a separately reviewed administrator/IaC workflow. Their service-specific definitions live in the private [v6-services-iac repository](https://github.com/topcoder-platform/v6-services-iac/tree/main/services), under `services/<repository>/deploy/iam/`. Application CI must not fetch that repository or provision IAM. Secret/KMS read permissions belong on the narrowly scoped execution role, never the deployment role.

An administrator installs the generated `trust` and `permissions` documents through reviewed infrastructure changes. Secret/KMS read permissions belong on the narrowly scoped execution role, never the deployment role.
`tcdeploy.py` exposes only `deploy` and `check-auth`; it does not generate or install IAM policies. Its STS session policy only restricts the temporary session within the existing role's permissions and boundary. It cannot grant new role permissions or change trust.

From the service checkout, in a build job without deployment context:

Expand Down Expand Up @@ -52,7 +50,7 @@ Plaintext runtime credentials remain forbidden. The specific public settings `M2

ECR may omit its optional severity-count map for a clean image. That response is accepted only after the same completion/freshness checks, with an exact registry/repository/digest match, explicitly empty findings lists, and no remaining page. An absent summary without that evidence, nonempty findings without a summary, or malformed counts fails closed. This requires no additional IAM permissions or changes to registry scanning. See the [ECR response schema](https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_ImageScanFindings.html).

Enhanced scanning also requires `inspector2:ListFindings` and `inspector2:ListCoverage` in the deployment role, its permissions boundary, and the STS session policy. The generated role policy limits these read-only actions to the deployment region. AWS does not support repository-level resource restrictions for these Inspector list actions, so they can read account-wide scan metadata in that region. No Inspector configuration/write actions or parameter/secret access are granted. Existing roles and boundaries must be updated through the private infrastructure repository before adopting this change. See [AWS enhanced-scanning permissions](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning-enhanced-iam.html).
Enhanced scanning also requires `inspector2:ListFindings` and `inspector2:ListCoverage` in the deployment role, its permissions boundary, and the STS session policy. The administrator-installed role policy and boundary must limit these read-only actions to the deployment region. AWS does not support repository-level resource restrictions for these Inspector list actions, so they can read account-wide scan metadata in that region. No Inspector configuration/write actions or parameter/secret access are granted. Existing roles and boundaries must be updated through the private infrastructure repository before adopting this change. See [AWS enhanced-scanning permissions](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning-enhanced-iam.html).

Use a unique CircleCI build number when retrying publication to immutable tags. ECR can return multiple image records when several tags share one digest; publication verification resolves the exact immutable release tag and checks its returned tag, image digest and configuration digest before scanning. A failed rollout remains a failure even if ECS rolls back. The script never blindly reverts over another deployment.

Expand Down
8 changes: 6 additions & 2 deletions docs/AUTHENTICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ The incident owner must locate the configured endpoint and associated Auth0 appl

## v2: CircleCI to STS directly

CircleCI issues an OIDC v3 token. AWS STS verifies it against the OIDC provider and role trust policy. The generated trust binds organization, project, checkout repository/ref **and configuration repository/ref**. Dev/prod use distinct roles/accounts/manifests. There is no Auth0 broker exchange, custom JWT credential envelope, or static AWS fallback.
CircleCI issues an OIDC v3 token. AWS STS verifies it against the OIDC provider and role trust policy. The pre-provisioned trust policy binds organization, project, checkout repository/ref **and configuration repository/ref**. Dev/prod use distinct roles/accounts/manifests. There is no Auth0 broker exchange, custom JWT credential envelope, or static AWS fallback.

The deployer requests a one-hour session, attaches a restrictive session policy, and verifies the returned account/assumed-role identity. Both the generated IAM role policy and session policy explicitly deny `ssm:*`, `secretsmanager:*`, `kms:Decrypt` and role chaining. Other permissions cover only required ECS/ECR operations and passing the two exact runtime roles. **The deny must be installed in IAM**: a hostile client could omit an optional session policy. Local code is not the durable authorization boundary.
The OIDC provider, roles, trust policies, permissions and boundaries are installed in advance with a separate administrator/IaC identity. Their definitions live in the private [v6-services-iac repository](https://github.com/topcoder-platform/v6-services-iac/tree/main/services), under `services/<repository>/deploy/iam/`. `tcdeploy.py` has no IAM policy-generation or installation command. Application CI does not fetch the IaC repository.

The `Policy` argument sent to STS is a restriction on a temporary session, not an IAM policy installation or role assignment. It cannot grant permissions beyond those allowed by the existing role and its boundary, or change the role's trust policy.

The deployer requests a one-hour session, attaches a restrictive session policy, and verifies the returned account/assumed-role identity. Both the administrator-installed IAM role policy and session policy explicitly deny `ssm:*`, `secretsmanager:*`, `kms:Decrypt` and role chaining. Other permissions cover only required ECS/ECR operations and passing the two exact runtime roles. **The deny must be installed in IAM**: a hostile client could omit an optional session policy. Local code is not the durable authorization boundary.

Non-secret targets live in reviewed JSON. Runtime parameter names/ARNs stay in the existing task definition. No deploy-time listing, fetching, decryption, shell export or parameter write occurs.

Expand Down
6 changes: 3 additions & 3 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Merging this PR does not change pinned v1 consumers or installed IAM policies. *
## Prepare one dev service

1. Start with a straightforward one-container service, for example lookups. Have an administrator inspect its existing ECS service/task, ECR settings and IAM roles without exporting decrypted SSM values. Record the previous revision/digest in a restricted release record.
2. Create a manifest with exact identifiers from that inspection. Generate IAM documents using `tcdeploy.py policies`; install them through infrastructure review. Keep the explicit secret-access/role-chaining deny in the role policy and preferably use the generated permissions as its permissions boundary too. Remove unrelated policies and audit resource-based grants. The deployer needs no infrastructure-creation permissions.
2. Create a manifest with exact identifiers from that inspection. Review the service-specific IAM definitions in the private [v6-services-iac repository](https://github.com/topcoder-platform/v6-services-iac/tree/main/services), under `services/<repository>/deploy/iam/`, and install them with a separate administrator/IaC identity before application deployment. Keep the explicit secret-access/role-chaining deny in both the role policy and its matching permissions boundary. Application CI must not fetch the IaC repository or provision IAM; `tcdeploy.py` has no policy-generation command. Remove unrelated policies and audit resource-based grants. The deployer needs no infrastructure-creation permissions.
3. Restrict PassRole to the exact task/execution roles (use JSON `null` for `taskRoleArn` when the existing task has no application role; do not add one just to deploy). Give the execution role only its runtime parameter/secret ARNs and necessary KMS keys. Pre-provision runtime bindings and defaults through administrator-owned infrastructure. v2 preserves references without fetching values/types: missing bindings fail at startup and trigger rollback.
4. Enable unconditional ECR tag immutability and supported basic/enhanced scan-on-push. Enable ECS circuit-breaker rollback, positive desired capacity and explicit task health checks. When the first new image introduces the health endpoint, pass a reviewed `--bootstrap-health-checks` file to add a missing check atomically with that image; do not restart the old image with a check it cannot serve. Existing differing checks cannot be overwritten. Remove plaintext secret environment values and `RESET_DB` secret bindings. Use non-root runtime images. Bootstrap/infrastructure updates remain separate from code deployment.
5. Commit non-secret `deploy/dev.json`. Use an exact `develop` or `dev` branch for dev and the exact reviewed `master` or legacy `prod` branch for production source/config refs. Examples intentionally do not guess real targets. Do not put keys, client secrets, passwords or parameter values in manifests.
Expand All @@ -18,7 +18,7 @@ For private packages, use a separate package-read-only identity and BuildKit sec
## Dev acceptance gates

- Run the offline tests and shell syntax checks. They do not prove installed IAM state.
- With an administrator identity, validate/simulate the generated policies. SSM listing/reads, Secrets Manager reads, direct KMS decryption and role chaining must be explicitly denied. Verify only the intended ECS service, ECR repositories, task family and two PassRole ARNs are allowed. Check OIDC audience/project/checkout/config origin/ref; a wrong project/ref must not assume the role. Do not weaken project restrictions to make a test pass.
- With an administrator identity, validate/simulate the reviewed IaC policies. SSM listing/reads, Secrets Manager reads, direct KMS decryption and role chaining must be explicitly denied. Verify only the intended ECS service, ECR repositories, task family and two PassRole ARNs are allowed. Check OIDC audience/project/checkout/config origin/ref; a wrong project/ref must not assume the role. Do not weaken project restrictions to make a test pass.
- Run `awsconfiguration.sh --manifest deploy/dev.json` in the intended job. It must verify identity without files/credential output. Missing v3 token, wrong account/role and legacy `DEV` arguments must fail.
- Run a real dev release. Verify roles, runtime secret references, sidecars, resources, volumes and existing health checks remain unchanged. If using health-check bootstrap, verify only the reviewed missing checks were added with the new image. Compare reported image digests with healthy running tasks. CloudTrail should show no deployment-principal SSM/Secrets Manager calls; runtime retrieval belongs to the execution role.
- Test an intentionally unhealthy dev image and verify failed release plus ECS rollback to the previous revision. Wrong digest, failed scan, denied AWS call, concurrency conflict or wrong source label must stop promotion. Old steady-state events/zero capacity must not pass.
Expand Down Expand Up @@ -63,4 +63,4 @@ Legacy SSM upload/setup and password-encryption helpers also fail closed. Secret

## Reviewed adapters for custom release gates

The default CLI and generated role policy still deploy an existing ECS service only. A checked-in service adapter may call `authenticate(..., additional_actions=...)` for the fixed allowlist in `ADAPTER_ACTIONS`: one-shot migration tasks, target health/Inspector reads, or updates to an existing CloudFormation stack. This is a Python API for reviewed code, not a pipeline parameter or CLI permission switch. The immutable session deny for SSM, Secrets Manager, KMS decryption and role chaining is retained. Each consumer must supply an administrator-installed role and permissions boundary with exact resource scopes for its additional actions. CloudFormation adapters must use the previous installed template and a separately scoped CloudFormation service role that also has no parameter/secret permissions; template installation belongs to infrastructure administration. No create-stack, arbitrary command, or secret-read capability is added to the default CLI.
The default CLI deploys an existing ECS service only. A checked-in service adapter may call `authenticate(..., additional_actions=...)` for the fixed allowlist in `ADAPTER_ACTIONS`: one-shot migration tasks, target health/Inspector reads, or updates to an existing CloudFormation stack. This is a Python API for reviewed code, not a pipeline parameter or CLI permission switch. The immutable session deny for SSM, Secrets Manager, KMS decryption and role chaining is retained. Each consumer must supply an administrator-installed role and permissions boundary with exact resource scopes for its additional actions. CloudFormation adapters must use the previous installed template and a separately scoped CloudFormation service role that also has no parameter/secret permissions; template installation belongs to infrastructure administration. No create-stack, arbitrary command, or secret-read capability is added to the default CLI.
49 changes: 3 additions & 46 deletions tcdeploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,48 +77,8 @@ def arn(m, service, resource):
return f"arn:aws:{service}:{m['region']}:{m['accountId']}:{resource}"


def role_policy(m):
"""Administrator-installed policy, also used as the deploy role boundary."""
return {'Version': '2012-10-17', 'Statement': [
{'Effect': 'Deny', 'Action': ['ssm:*', 'secretsmanager:*', 'kms:Decrypt', 'sts:AssumeRole*'], 'Resource': '*'},
# DescribeTaskDefinition does not support resource-level authorization.
{'Effect': 'Allow', 'Action': ['sts:GetCallerIdentity', 'ecr:GetAuthorizationToken',
'ecs:DescribeTaskDefinition'], 'Resource': '*'},
{'Effect': 'Allow', 'Action': ['ecr:DescribeRepositories', 'ecr:DescribeImages',
'ecr:BatchGetImage', 'ecr:BatchCheckLayerAvailability', 'ecr:InitiateLayerUpload',
'ecr:UploadLayerPart', 'ecr:CompleteLayerUpload', 'ecr:PutImage',
'ecr:DescribeImageScanFindings'],
'Resource': [arn(m, 'ecr', 'repository/' + r) for r in m['images'].values()]},
{'Effect': 'Allow', 'Action': ['ecs:DescribeServices'], 'Resource': arn(m, 'ecs', f"service/{m['cluster']}/{m['service']}")},
{'Effect': 'Allow', 'Action': 'ecs:UpdateService', 'Resource': arn(m, 'ecs', f"service/{m['cluster']}/{m['service']}"),
'Condition': {'ArnLike': {'ecs:task-definition': arn(m, 'ecs', 'task-definition/' + m['taskFamily'] + ':*')}}},
{'Effect': 'Allow', 'Action': ['ecs:RegisterTaskDefinition', 'ecs:TagResource', 'ecs:ListTagsForResource'],
'Resource': arn(m, 'ecs', 'task-definition/' + m['taskFamily'] + ':*')},
{'Effect': 'Allow', 'Action': ['ecs:ListTasks', 'ecs:DescribeTasks'], 'Resource': '*',
'Condition': {'ArnEquals': {'ecs:cluster': arn(m, 'ecs', 'cluster/' + m['cluster'])}}},
# ECR enhanced scan reads depend on these Inspector APIs. They do not
# support resource-level permissions; limit them to the deployment region.
{'Effect': 'Allow', 'Action': ['inspector2:ListFindings', 'inspector2:ListCoverage'], 'Resource': '*',
'Condition': {'StringEquals': {'aws:RequestedRegion': m['region']}}},
{'Effect': 'Allow', 'Action': 'iam:PassRole', 'Resource': [r for r in (m['taskRoleArn'], m['executionRoleArn']) if r],
'Condition': {'StringEquals': {'iam:PassedToService': 'ecs-tasks.amazonaws.com'}}}
]}


def trust_policy(m):
issuer = 'oidc.circleci.com/org/' + m['circleciOrgId']
subject = (f"org:{m['circleciOrgId']}:project:{m['circleciProjectId']}:user:*:"
f"checkout-source:{m['repository']}:checkout-ref:refs/heads/{m['branch']}:"
f"config-source:{m['repository']}:config-ref:refs/heads/{m['branch']}")
return {'Version': '2012-10-17', 'Statement': [{
'Effect': 'Allow', 'Principal': {'Federated': f"arn:aws:iam::{m['accountId']}:oidc-provider/{issuer}"},
'Action': 'sts:AssumeRoleWithWebIdentity', 'Condition': {
'StringEquals': {issuer + ':aud': m['circleciOrgId']},
'StringLike': {issuer + ':sub': subject}}
}]}


# Compact STS policy (<2048 bytes). IAM role policy above restricts resources.
# Restrict the temporary STS session; this does not create or modify IAM policies.
# Pre-provisioned IAM roles and boundaries in v6-services-iac scope resources.
# Explicit deny also protects against resource policies and prevents role chaining.
SESSION_POLICY = {'Version': '2012-10-17', 'Statement': [
{'Effect': 'Deny', 'Action': ['ssm:*', 'secretsmanager:*', 'kms:Decrypt', 'sts:AssumeRole*'], 'Resource': '*'},
Expand Down Expand Up @@ -556,7 +516,7 @@ def deploy(aws, m, locals_by_name, release, publisher, health_checks=None):
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest='command', required=True)
for name in ('deploy', 'policies', 'check-auth'):
for name in ('deploy', 'check-auth'):
p = sub.add_parser(name)
p.add_argument('--manifest', required=True)
if name == 'deploy':
Expand All @@ -568,9 +528,6 @@ def main(argv=None):
if args.command == 'deploy' and args.bootstrap_health_checks:
health_checks = validate_health_checks(m, json.loads(
Path(args.bootstrap_health_checks).read_text(), object_pairs_hook=unique_object))
if args.command == 'policies':
print(json.dumps({'trust': trust_policy(m), 'permissions': role_policy(m)}, indent=2))
return
require(os.environ.get('CIRCLE_BRANCH') == m['branch'] and not os.environ.get('CIRCLE_TAG'), 'Only the configured branch may deploy (tag pipelines are forbidden)')
require(os.environ.get('CIRCLE_PROJECT_USERNAME', '') + '/' + os.environ.get('CIRCLE_PROJECT_REPONAME', '') == m['repository'].removeprefix('github.com/'), 'CircleCI repository differs from manifest')
token = os.environ.get('CIRCLE_OIDC_TOKEN_V3')
Expand Down
Loading
Loading