feat(P31): encryption-by-default + per-stack CMK (REQ-83, REQ-84, REQ-85)
---ci--- project: acdl phase: 31 milestone: v1.8 status: execute ---/ci--- - New kms-key L1 primitive (aws:kms:key) with enable_key_rotation=true (AWS-managed annual rotation, D-075). Registered in registry.json. - Adapter TYPE_MAP expanded for aws:kms:key + aws:kms:alias. - Adapter emits enable_key_rotation from NFR. - S3 adapter emits server_side_encryption_configuration with KMS when kms_key_arn provided; managed KMS fallback with stderr warning when not. - All 10 existing L1 primitives now have encryption_enabled NFR (default true). - s3, rds, ecr, ecs-service, ecs-cluster have kms_key_arn input. - Both L2 compositions (static-assets, microservice) now include a kms-key child + wires connecting kms_key_arn to children. - L2 stack outputs include kms_key_arn. Tests: +7 (300 -> 307). All pass. run_platform.sh --check-only green (static-assets now resolves to 5 resources with the CMK).
This commit is contained in:
@@ -40,6 +40,8 @@ TYPE_MAP = {
|
|||||||
"aws:cloudfront:originaccesscontrol": "aws_cloudfront_origin_access_control",
|
"aws:cloudfront:originaccesscontrol": "aws_cloudfront_origin_access_control",
|
||||||
"aws:wafv2:webacl": "aws_wafv2_web_acl",
|
"aws:wafv2:webacl": "aws_wafv2_web_acl",
|
||||||
"aws:rds:instance": "aws_db_instance",
|
"aws:rds:instance": "aws_db_instance",
|
||||||
|
"aws:kms:key": "aws_kms_key",
|
||||||
|
"aws:kms:alias": "aws_kms_alias",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stack input name -> Terraform arg name, per stack type. Only non-identity
|
# Stack input name -> Terraform arg name, per stack type. Only non-identity
|
||||||
@@ -62,6 +64,8 @@ INPUT_MAP = {
|
|||||||
"aws:cloudfront:originaccesscontrol": {"name": "name", "origin_type": "origin_access_control_origin_type", "signing_behavior": "origin_access_control_signing_behavior"},
|
"aws:cloudfront:originaccesscontrol": {"name": "name", "origin_type": "origin_access_control_origin_type", "signing_behavior": "origin_access_control_signing_behavior"},
|
||||||
"aws:wafv2:webacl": {"name": "name", "scope": "scope", "default_action": "default_action", "rules": "rules"},
|
"aws:wafv2:webacl": {"name": "name", "scope": "scope", "default_action": "default_action", "rules": "rules"},
|
||||||
"aws:rds:instance": {"db_name": "db_name", "instance_class": "instance_class", "allocated_storage": "allocated_storage", "engine": "engine", "engine_version": "engine_version", "username": "username", "multi_az": "multi_az", "storage_encrypted": "storage_encrypted"},
|
"aws:rds:instance": {"db_name": "db_name", "instance_class": "instance_class", "allocated_storage": "allocated_storage", "engine": "engine", "engine_version": "engine_version", "username": "username", "multi_az": "multi_az", "storage_encrypted": "storage_encrypted"},
|
||||||
|
"aws:kms:key": {"description": "description", "deletion_window_days": "deletion_window_in_days"},
|
||||||
|
"aws:kms:alias": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stack output name -> Terraform attribute name, per stack type. Only
|
# Stack output name -> Terraform attribute name, per stack type. Only
|
||||||
@@ -84,6 +88,8 @@ OUTPUT_MAP = {
|
|||||||
"aws:cloudfront:originaccesscontrol": {"oac_id": "id"},
|
"aws:cloudfront:originaccesscontrol": {"oac_id": "id"},
|
||||||
"aws:wafv2:webacl": {"web_acl_arn": "arn"},
|
"aws:wafv2:webacl": {"web_acl_arn": "arn"},
|
||||||
"aws:rds:instance": {"db_endpoint": "endpoint", "db_arn": "arn"},
|
"aws:rds:instance": {"db_endpoint": "endpoint", "db_arn": "arn"},
|
||||||
|
"aws:kms:key": {"kms_key_arn": "arn", "kms_key_id": "key_id"},
|
||||||
|
"aws:kms:alias": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -420,6 +426,44 @@ def _emit_resource(resource, type_by_id=None):
|
|||||||
# Dev safety: skip the final snapshot so `terraform destroy` works
|
# Dev safety: skip the final snapshot so `terraform destroy` works
|
||||||
# without a final DB snapshot (overridden by deletion_protection).
|
# without a final DB snapshot (overridden by deletion_protection).
|
||||||
body.append("skip_final_snapshot = true")
|
body.append("skip_final_snapshot = true")
|
||||||
|
if rtype == "aws:kms:key":
|
||||||
|
nfrs = resource.get("nfrs", {})
|
||||||
|
enable_rotation = nfrs.get("enable_rotation", True)
|
||||||
|
body.append(f"enable_key_rotation = {_tf_value(enable_rotation)}")
|
||||||
|
if rtype == "aws:s3:bucket":
|
||||||
|
nfrs = resource.get("nfrs", {})
|
||||||
|
encryption_enabled = nfrs.get("encryption_enabled", True)
|
||||||
|
if encryption_enabled:
|
||||||
|
kms_key_arn = inputs.get("kms_key_arn")
|
||||||
|
if kms_key_arn and isinstance(kms_key_arn, str) and kms_key_arn.startswith("ref:"):
|
||||||
|
kms_ref = _ref_expr(kms_key_arn, type_by_id)
|
||||||
|
body.append("server_side_encryption_configuration {")
|
||||||
|
body.append(" rule {")
|
||||||
|
body.append(" apply_server_side_encryption_by_default {")
|
||||||
|
body.append(f" sse_algorithm = \"aws:kms\"")
|
||||||
|
body.append(f" kms_master_key_id = {kms_ref}")
|
||||||
|
body.append(" }")
|
||||||
|
body.append(" }")
|
||||||
|
body.append("}")
|
||||||
|
elif kms_key_arn:
|
||||||
|
body.append("server_side_encryption_configuration {")
|
||||||
|
body.append(" rule {")
|
||||||
|
body.append(" apply_server_side_encryption_by_default {")
|
||||||
|
body.append(" sse_algorithm = \"aws:kms\"")
|
||||||
|
body.append(f" kms_master_key_id = {_tf_value(kms_key_arn)}")
|
||||||
|
body.append(" }")
|
||||||
|
body.append(" }")
|
||||||
|
body.append("}")
|
||||||
|
else:
|
||||||
|
import sys
|
||||||
|
print(f"WARNING: s3 bucket {rid} has no kms_key_arn — falling back to AWS-managed key (alias/aws/s3)", file=sys.stderr)
|
||||||
|
body.append("server_side_encryption_configuration {")
|
||||||
|
body.append(" rule {")
|
||||||
|
body.append(" apply_server_side_encryption_by_default {")
|
||||||
|
body.append(" sse_algorithm = \"aws:kms\"")
|
||||||
|
body.append(" }")
|
||||||
|
body.append(" }")
|
||||||
|
body.append("}")
|
||||||
return _resource_block(rid, tf_type, body)
|
return _resource_block(rid, tf_type, body)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,18 @@
|
|||||||
"description": "The target group ARN."
|
"description": "The target group ARN."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {},
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable TLS/HTTPS encryption in transit.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"tls_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable TLS listener.",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"type": "aws:elbv2:loadbalancer",
|
"type": "aws:elbv2:loadbalancer",
|
||||||
|
|||||||
@@ -59,7 +59,13 @@
|
|||||||
"description": "The Origin Access Control ID."
|
"description": "The Origin Access Control ID."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {},
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable encryption in transit (HTTPS only).",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"type": "aws:cloudfront:distribution",
|
"type": "aws:cloudfront:distribution",
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "AWS region the repository is created in.",
|
"description": "AWS region the repository is created in.",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
"kms_key_arn": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ARN of the CMK for repository encryption; if absent, uses AWS-managed key.",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
@@ -26,5 +31,16 @@
|
|||||||
"description": "The ECR repository ARN."
|
"description": "The ECR repository ARN."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {}
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable repository encryption (KMS).",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"encryption_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Encryption type.",
|
||||||
|
"default": "KMS"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "AWS region the cluster is created in.",
|
"description": "AWS region the cluster is created in.",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
"kms_key_arn": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ARN of the CMK for CloudWatch log group encryption; if absent, uses managed key.",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
@@ -26,5 +31,11 @@
|
|||||||
"description": "The ECS cluster id (name)."
|
"description": "The ECS cluster id (name)."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {}
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable CloudWatch log group encryption.",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -56,6 +56,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "AWS region the service is created in.",
|
"description": "AWS region the service is created in.",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
"kms_key_arn": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ARN of the CMK for CloudWatch log group encryption; if absent, uses managed key.",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
@@ -68,7 +73,13 @@
|
|||||||
"description": "The ECS task definition ARN."
|
"description": "The ECS task definition ARN."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {},
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable CloudWatch log group encryption.",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"type": "aws:ecs:task_definition",
|
"type": "aws:ecs:task_definition",
|
||||||
|
|||||||
@@ -36,5 +36,11 @@
|
|||||||
"description": "The IAM role id."
|
"description": "The IAM role id."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {}
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Encryption is not applicable to IAM roles but included for standards compliance.",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# kms-key — KMS customer-managed key
|
||||||
|
|
||||||
|
> **Module kind:** primitive | **Version:** 1.0.0
|
||||||
|
|
||||||
|
A customer-managed KMS key for per-stack encryption. Created with key
|
||||||
|
rotation enabled. One key per L2 deployment (no shared keys).
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
| Resource | Type | Purpose |
|
||||||
|
|----------|------|---------|
|
||||||
|
| kms-key | `aws_kms_key` | The KMS customer-managed key |
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
| Name | Type | Required | Default | Description |
|
||||||
|
|------|------|----------|---------|-------------|
|
||||||
|
| `description` | string | yes | — | Description of the KMS key |
|
||||||
|
| `region` | string | yes | — | AWS region the KMS key is created in |
|
||||||
|
| `deletion_window_days` | number | no | 30 | Number of days before the key is deleted after deletion is requested |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `kms_key_arn` | arn | The ARN of the KMS key |
|
||||||
|
| `kms_key_id` | string | The ID of the KMS key |
|
||||||
|
|
||||||
|
## NFRs
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `enable_rotation` | boolean | true | Enable automatic key rotation |
|
||||||
|
| `deletion_protection` | boolean | true | Prevent key destruction |
|
||||||
|
| `encryption_enabled` | boolean | true | Encryption is always enabled for a KMS key |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "kms-key",
|
||||||
|
"type": "aws:kms:key",
|
||||||
|
"module": "kms-key@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"description": "ACDL per-stack CMK",
|
||||||
|
"region": "us-east-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A concrete instance is at `instance.json` (used by the platform
|
||||||
|
pipeline as the regression baseline).
|
||||||
|
|
||||||
|
## Compliance extension points
|
||||||
|
|
||||||
|
- **Key rotation** — automatic key rotation enabled by default (SOC2 CC6.1, HIPAA §164.312(a)(2)(iv), GDPR Art.32).
|
||||||
|
- **Deletion protection** — pending deletion window prevents accidental destruction (SOC2 CC7.2).
|
||||||
|
- **Key policy** — restrict key usage to the stack's IAM roles (SOC2 CC6.1, GDPR Art.32).
|
||||||
|
- **Audit logging** — CloudTrail logs all KMS API calls (SOC2 CC7.2, DORA audit trail).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Validated example contracts are in [`examples/`](examples/). The platform-test
|
||||||
|
pipeline validates them against `schemas/contract.schema.json`.
|
||||||
|
|
||||||
|
### Simple
|
||||||
|
|
||||||
|
A minimal deployment:
|
||||||
|
|
||||||
|
[`examples/simple.yaml`](examples/simple.yaml)
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||||
|
module: kms-key
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
description: "Simple CMK for testing"
|
||||||
|
region: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex
|
||||||
|
|
||||||
|
A production deployment with optional inputs:
|
||||||
|
|
||||||
|
[`examples/complex.yaml`](examples/complex.yaml)
|
||||||
|
```yaml
|
||||||
|
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||||
|
module: kms-key
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
description: "Production CMK with 90-day deletion window"
|
||||||
|
region: us-east-1
|
||||||
|
deletion_window_days: 90
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||||
|
require a new registry entry (immutable publication); old entries enter
|
||||||
|
a 12-month deprecation window.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||||
|
module: kms-key
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
description: "Production CMK with 90-day deletion window"
|
||||||
|
region: us-east-1
|
||||||
|
deletion_window_days: 90
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
uses: acdl/pipelines/deploy.yaml@v1.8
|
||||||
|
module: kms-key
|
||||||
|
environment: dev
|
||||||
|
inputs:
|
||||||
|
description: "Simple CMK for testing"
|
||||||
|
region: us-east-1
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {
|
||||||
|
"name": "kms-key",
|
||||||
|
"kind": "l1",
|
||||||
|
"depth": 1
|
||||||
|
},
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "kms-key",
|
||||||
|
"type": "aws:kms:key",
|
||||||
|
"module": "kms-key@1.0.0",
|
||||||
|
"inputs": {
|
||||||
|
"description": "ACDL per-stack CMK",
|
||||||
|
"region": "us-east-1",
|
||||||
|
"deletion_window_days": 30
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"kms_key_arn": {
|
||||||
|
"type": "arn"
|
||||||
|
},
|
||||||
|
"kms_key_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nfrs": {
|
||||||
|
"enable_rotation": true,
|
||||||
|
"deletion_protection": true,
|
||||||
|
"encryption_enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "kms-key",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"kind": "l1",
|
||||||
|
"type": "aws:kms:key",
|
||||||
|
"description": "A customer-managed KMS key for per-stack encryption. Created with key rotation enabled. One key per L2 deployment (no shared keys).",
|
||||||
|
"inputs": {
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Description of the KMS key.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "AWS region the KMS key is created in.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"deletion_window_days": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Number of days before the key is deleted after deletion is requested (default 30).",
|
||||||
|
"required": false,
|
||||||
|
"default": 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"kms_key_arn": {
|
||||||
|
"type": "arn",
|
||||||
|
"description": "The ARN of the KMS key."
|
||||||
|
},
|
||||||
|
"kms_key_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The ID of the KMS key."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nfrs": {
|
||||||
|
"enable_rotation": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable automatic key rotation.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"deletion_protection": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Prevent key destruction.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Encryption is always enabled for a KMS key.",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "AWS region the RDS instance is created in.",
|
"description": "AWS region the RDS instance is created in.",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
"kms_key_arn": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ARN of the CMK for storage encryption; if absent, uses AWS-managed key.",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
@@ -76,6 +81,11 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Enable deletion protection (default true for prod).",
|
"description": "Enable deletion protection (default true for prod).",
|
||||||
"default": true
|
"default": true
|
||||||
|
},
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable storage encryption.",
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "AWS region the bucket is created in.",
|
"description": "AWS region the bucket is created in.",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
"kms_key_arn": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ARN of the CMK for SSE-KMS; if absent, uses managed key.",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
@@ -35,6 +40,16 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Enable S3 versioning (default true).",
|
"description": "Enable S3 versioning (default true).",
|
||||||
"default": true
|
"default": true
|
||||||
|
},
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable server-side encryption.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"sse_algorithm": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "SSE algorithm.",
|
||||||
|
"default": "aws:kms"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,18 @@
|
|||||||
"description": "Comma-separated subnet ids."
|
"description": "Comma-separated subnet ids."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {},
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable KMS-encrypted VPC flow logs.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"flow_logs_encrypted": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Encrypt VPC flow logs with KMS.",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"type": "aws:ec2:vpc",
|
"type": "aws:ec2:vpc",
|
||||||
|
|||||||
@@ -39,7 +39,18 @@
|
|||||||
"description": "The WAF Web ACL ARN."
|
"description": "The WAF Web ACL ARN."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nfrs": {},
|
"nfrs": {
|
||||||
|
"encryption_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable KMS-encrypted CloudWatch log group for WAF logs.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"logging_enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Enable WAF logging.",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"resources": [
|
"resources": [
|
||||||
{
|
{
|
||||||
"type": "aws:wafv2:webacl",
|
"type": "aws:wafv2:webacl",
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
{"id": "ecr", "module": "ecr@1.0.0"},
|
{"id": "ecr", "module": "ecr@1.0.0"},
|
||||||
{"id": "roles", "module": "iam-role@1.0.0"},
|
{"id": "roles", "module": "iam-role@1.0.0"},
|
||||||
{"id": "alb", "module": "alb@1.0.0"},
|
{"id": "alb", "module": "alb@1.0.0"},
|
||||||
{"id": "service", "module": "ecs-service@1.0.0"}
|
{"id": "service", "module": "ecs-service@1.0.0"},
|
||||||
|
{"id": "kms", "module": "kms-key@1.0.0"}
|
||||||
],
|
],
|
||||||
"wires": [
|
"wires": [
|
||||||
{"from": "contract.inputs.bucket_name", "to": "vpc.inputs.cidr", "default": "10.0.0.0/16"},
|
{"from": "contract.inputs.bucket_name", "to": "vpc.inputs.cidr", "default": "10.0.0.0/16"},
|
||||||
@@ -25,10 +26,13 @@
|
|||||||
{"from": "cluster.outputs.cluster_arn", "to": "service.inputs.cluster_arn"},
|
{"from": "cluster.outputs.cluster_arn", "to": "service.inputs.cluster_arn"},
|
||||||
{"from": "ecr.outputs.repository_url", "to": "service.inputs.image"},
|
{"from": "ecr.outputs.repository_url", "to": "service.inputs.image"},
|
||||||
{"from": "roles.outputs.role_arn", "to": "service.inputs.security_group"},
|
{"from": "roles.outputs.role_arn", "to": "service.inputs.security_group"},
|
||||||
{"from": "alb.outputs.target_group_arn", "to": "service.inputs.lb_target_group_arn"}
|
{"from": "alb.outputs.target_group_arn", "to": "service.inputs.lb_target_group_arn"},
|
||||||
|
{"from": "contract.inputs.region", "to": "kms.inputs.region"},
|
||||||
|
{"from": "kms.outputs.kms_key_arn", "to": "ecr.inputs.kms_key_arn"}
|
||||||
],
|
],
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{"from": "alb.outputs.lb_arn", "to": "stack.outputs.lb_arn"},
|
{"from": "alb.outputs.lb_arn", "to": "stack.outputs.lb_arn"},
|
||||||
{"from": "service.outputs.service_arn", "to": "stack.outputs.service_arn"}
|
{"from": "service.outputs.service_arn", "to": "stack.outputs.service_arn"},
|
||||||
|
{"from": "kms.outputs.kms_key_arn", "to": "stack.outputs.kms_key_arn"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
"children": [
|
"children": [
|
||||||
{"id": "s3", "module": "s3@1.0.0"},
|
{"id": "s3", "module": "s3@1.0.0"},
|
||||||
{"id": "cloudfront", "module": "cloudfront@1.0.0"},
|
{"id": "cloudfront", "module": "cloudfront@1.0.0"},
|
||||||
{"id": "waf", "module": "waf@1.0.0"}
|
{"id": "waf", "module": "waf@1.0.0"},
|
||||||
|
{"id": "kms", "module": "kms-key@1.0.0"}
|
||||||
],
|
],
|
||||||
"wires": [
|
"wires": [
|
||||||
{"from": "contract.inputs.bucket_name", "to": "s3.inputs.bucket_name"},
|
{"from": "contract.inputs.bucket_name", "to": "s3.inputs.bucket_name"},
|
||||||
@@ -15,11 +16,14 @@
|
|||||||
{"from": "contract.inputs.region", "to": "cloudfront.inputs.region"},
|
{"from": "contract.inputs.region", "to": "cloudfront.inputs.region"},
|
||||||
{"from": "contract.inputs.region", "to": "waf.inputs.region"},
|
{"from": "contract.inputs.region", "to": "waf.inputs.region"},
|
||||||
{"from": "s3.outputs.bucket_regional_domain_name", "to": "cloudfront.inputs.bucket_regional_domain_name"},
|
{"from": "s3.outputs.bucket_regional_domain_name", "to": "cloudfront.inputs.bucket_regional_domain_name"},
|
||||||
{"from": "waf.outputs.web_acl_arn", "to": "cloudfront.inputs.waf_web_acl_arn"}
|
{"from": "waf.outputs.web_acl_arn", "to": "cloudfront.inputs.waf_web_acl_arn"},
|
||||||
|
{"from": "contract.inputs.region", "to": "kms.inputs.region"},
|
||||||
|
{"from": "kms.outputs.kms_key_arn", "to": "s3.inputs.kms_key_arn"}
|
||||||
],
|
],
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{"from": "cloudfront.outputs.distribution_domain_name", "to": "stack.outputs.distribution_domain_name"},
|
{"from": "cloudfront.outputs.distribution_domain_name", "to": "stack.outputs.distribution_domain_name"},
|
||||||
{"from": "s3.outputs.bucket_arn", "to": "stack.outputs.bucket_arn"},
|
{"from": "s3.outputs.bucket_arn", "to": "stack.outputs.bucket_arn"},
|
||||||
{"from": "waf.outputs.web_acl_arn", "to": "stack.outputs.web_acl_arn"}
|
{"from": "waf.outputs.web_acl_arn", "to": "stack.outputs.web_acl_arn"},
|
||||||
|
{"from": "kms.outputs.kms_key_arn", "to": "stack.outputs.kms_key_arn"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -69,6 +69,13 @@
|
|||||||
"deprecated": false
|
"deprecated": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"kms-key": {
|
||||||
|
"1.0.0": {
|
||||||
|
"interface": "modules/l1/kms-key/interface.json",
|
||||||
|
"published_at": "2026-07-22T20:00",
|
||||||
|
"deprecated": false
|
||||||
|
}
|
||||||
|
},
|
||||||
"static-assets": {
|
"static-assets": {
|
||||||
"1.0.0": {
|
"1.0.0": {
|
||||||
"interface": "modules/l2/static-assets/composition.json",
|
"interface": "modules/l2/static-assets/composition.json",
|
||||||
|
|||||||
+98
-4
@@ -31,14 +31,14 @@ class TestInstance:
|
|||||||
|
|
||||||
|
|
||||||
class TestRegistry:
|
class TestRegistry:
|
||||||
EXPECTED_L1_KEYS = {"s3", "vpc", "ecs-cluster", "ecs-service", "iam-role", "alb", "ecr", "cloudfront", "waf", "rds"}
|
EXPECTED_L1_KEYS = {"s3", "vpc", "ecs-cluster", "ecs-service", "iam-role", "alb", "ecr", "cloudfront", "waf", "rds", "kms-key"}
|
||||||
EXPECTED_L2_KEYS = {"static-assets", "microservice"}
|
EXPECTED_L2_KEYS = {"static-assets", "microservice"}
|
||||||
|
|
||||||
def test_registry_has_12_entries(self, registry):
|
def test_registry_has_13_entries(self, registry):
|
||||||
assert len(registry) == 12
|
assert len(registry) == 13
|
||||||
assert set(registry.keys()) == (self.EXPECTED_L1_KEYS | self.EXPECTED_L2_KEYS)
|
assert set(registry.keys()) == (self.EXPECTED_L1_KEYS | self.EXPECTED_L2_KEYS)
|
||||||
|
|
||||||
def test_registry_has_10_l1_entries(self, registry):
|
def test_registry_has_11_l1_entries(self, registry):
|
||||||
l1 = {k for k in registry if registry[k]["1.0.0"]["interface"].startswith("modules/l1/")}
|
l1 = {k for k in registry if registry[k]["1.0.0"]["interface"].startswith("modules/l1/")}
|
||||||
assert l1 == self.EXPECTED_L1_KEYS
|
assert l1 == self.EXPECTED_L1_KEYS
|
||||||
|
|
||||||
@@ -402,3 +402,97 @@ class TestResolverOutputs:
|
|||||||
assert 'output "distribution_domain_name"' in main_tf
|
assert 'output "distribution_domain_name"' in main_tf
|
||||||
assert 'output "bucket_arn"' in main_tf
|
assert 'output "bucket_arn"' in main_tf
|
||||||
assert 'output "web_acl_arn"' in main_tf
|
assert 'output "web_acl_arn"' in main_tf
|
||||||
|
|
||||||
|
|
||||||
|
class TestEncryptionByDefault:
|
||||||
|
"""REQ-83/84/85: encryption by default + per-stack CMK."""
|
||||||
|
|
||||||
|
def test_kms_key_primitive_in_registry(self, registry):
|
||||||
|
assert "kms-key" in registry
|
||||||
|
|
||||||
|
def test_kms_key_interface_validates(self, repo_root):
|
||||||
|
iface_path = os.path.join(str(repo_root), "modules", "l1", "kms-key", "interface.json")
|
||||||
|
iface = json.load(open(iface_path))
|
||||||
|
assert iface["type"] == "aws:kms:key"
|
||||||
|
assert "enable_rotation" in iface["nfrs"]
|
||||||
|
assert iface["nfrs"]["enable_rotation"]["default"] is True
|
||||||
|
|
||||||
|
def test_kms_key_adapter_emits_rotation(self, tmp_path):
|
||||||
|
kms_stack = {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {"name": "kms-key", "kind": "l1", "depth": 1},
|
||||||
|
"resources": [{
|
||||||
|
"id": "kms-key",
|
||||||
|
"type": "aws:kms:key",
|
||||||
|
"module": "kms-key@1.0.0",
|
||||||
|
"inputs": {"description": "test key", "region": "us-east-1", "deletion_window_days": 30},
|
||||||
|
"outputs": {},
|
||||||
|
"nfrs": {"enable_rotation": True, "deletion_protection": True, "encryption_enabled": True},
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
out_dir = str(tmp_path / "tf_out")
|
||||||
|
adapt(kms_stack, out_dir)
|
||||||
|
main_tf = open(os.path.join(out_dir, "main.tf")).read()
|
||||||
|
assert 'resource "aws_kms_key" "kms-key"' in main_tf
|
||||||
|
assert "enable_key_rotation = true" in main_tf
|
||||||
|
|
||||||
|
def test_all_l1_primitives_have_encryption_nfr(self, registry, repo_root):
|
||||||
|
"""REQ-84: every L1 primitive must have an encryption_enabled NFR."""
|
||||||
|
for name, entry in registry.items():
|
||||||
|
iface_path = entry["1.0.0"]["interface"]
|
||||||
|
if not iface_path.startswith("modules/l1/"):
|
||||||
|
continue
|
||||||
|
iface = json.load(open(os.path.join(str(repo_root), iface_path)))
|
||||||
|
assert "encryption_enabled" in iface.get("nfrs", {}), \
|
||||||
|
f"L1 primitive '{name}' must have encryption_enabled NFR"
|
||||||
|
|
||||||
|
def test_s3_with_kms_key_arn_emits_sse_configuration(self, tmp_path):
|
||||||
|
s3_stack = {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {"name": "s3-test", "kind": "l1", "depth": 1},
|
||||||
|
"resources": [{
|
||||||
|
"id": "s3",
|
||||||
|
"type": "aws:s3:bucket",
|
||||||
|
"module": "s3@1.0.0",
|
||||||
|
"inputs": {"bucket_name": "test-bucket", "region": "us-east-1", "kms_key_arn": "arn:aws:kms:us-east-1:123:key/abc"},
|
||||||
|
"outputs": {},
|
||||||
|
"nfrs": {"encryption_enabled": True, "versioning": True},
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
out_dir = str(tmp_path / "tf_out")
|
||||||
|
adapt(s3_stack, out_dir)
|
||||||
|
main_tf = open(os.path.join(out_dir, "main.tf")).read()
|
||||||
|
assert "server_side_encryption_configuration" in main_tf
|
||||||
|
assert "aws:kms" in main_tf
|
||||||
|
assert "arn:aws:kms:us-east-1:123:key/abc" in main_tf
|
||||||
|
|
||||||
|
def test_s3_without_kms_key_arn_falls_back_to_managed(self, tmp_path, capsys):
|
||||||
|
s3_stack = {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stack": {"name": "s3-test", "kind": "l1", "depth": 1},
|
||||||
|
"resources": [{
|
||||||
|
"id": "s3",
|
||||||
|
"type": "aws:s3:bucket",
|
||||||
|
"module": "s3@1.0.0",
|
||||||
|
"inputs": {"bucket_name": "test-bucket", "region": "us-east-1"},
|
||||||
|
"outputs": {},
|
||||||
|
"nfrs": {"encryption_enabled": True, "versioning": True},
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
out_dir = str(tmp_path / "tf_out")
|
||||||
|
adapt(s3_stack, out_dir)
|
||||||
|
main_tf = open(os.path.join(out_dir, "main.tf")).read()
|
||||||
|
assert "server_side_encryption_configuration" in main_tf
|
||||||
|
assert "aws:kms" in main_tf
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "WARNING" in captured.err or "falling back" in captured.err
|
||||||
|
|
||||||
|
def test_static_assets_l2_wires_kms_key_to_s3(self):
|
||||||
|
"""REQ-85: L2 modules wire per-stack CMK to children."""
|
||||||
|
from core.contract_resolver import resolve
|
||||||
|
stack = resolve(str(ROOT / "contracts/static-assets.yaml"), str(ROOT))
|
||||||
|
types = [r["type"] for r in stack["resources"]]
|
||||||
|
assert "aws:kms:key" in types
|
||||||
|
s3_res = next(r for r in stack["resources"] if r["type"] == "aws:s3:bucket")
|
||||||
|
assert "kms_key_arn" in s3_res.get("inputs", {}), \
|
||||||
|
"s3 must have kms_key_arn wired from the per-stack CMK"
|
||||||
Reference in New Issue
Block a user