thinkwork-cli 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +2 -2
- package/dist/cli.js +1187 -315
- package/dist/terraform/examples/greenfield/main.tf +325 -19
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +14 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +61 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +54 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +197 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +70 -0
- package/dist/terraform/modules/app/agentcore-flue/README.md +58 -0
- package/dist/terraform/modules/app/agentcore-flue/main.tf +322 -0
- package/dist/terraform/modules/app/agentcore-flue/outputs.tf +23 -0
- package/dist/terraform/modules/app/agentcore-flue/variables.tf +91 -0
- package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +0 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +165 -0
- package/dist/terraform/modules/app/appsync-subscriptions/main.tf +4 -0
- package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +5 -0
- package/dist/terraform/modules/app/computer-runtime/README.md +15 -0
- package/dist/terraform/modules/app/computer-runtime/main.tf +406 -0
- package/dist/terraform/modules/app/computer-runtime/outputs.tf +75 -0
- package/dist/terraform/modules/app/computer-runtime/variables.tf +66 -0
- package/dist/terraform/modules/app/hindsight-memory/main.tf +6 -0
- package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +128 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +1454 -43
- package/dist/terraform/modules/app/lambda-api/main.tf +221 -12
- package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +118 -0
- package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +49 -0
- package/dist/terraform/modules/app/lambda-api/outputs.tf +38 -0
- package/dist/terraform/modules/app/lambda-api/slack-app-secrets.tf +43 -0
- package/dist/terraform/modules/app/lambda-api/stripe-secrets.tf +53 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +349 -2
- package/dist/terraform/modules/app/lambda-api/workspace-events.tf +125 -0
- package/dist/terraform/modules/app/routines-stepfunctions/main.tf +453 -0
- package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +66 -0
- package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +200 -0
- package/dist/terraform/modules/app/static-site/main.tf +146 -5
- package/dist/terraform/modules/app/www-dns/main.tf +118 -15
- package/dist/terraform/modules/app/www-dns/outputs.tf +10 -0
- package/dist/terraform/modules/app/www-dns/variables.tf +42 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +164 -3
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +34 -0
- package/dist/terraform/modules/data/aurora-postgres/variables.tf +16 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/README.md +145 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/main.tf +573 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/outputs.tf +43 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/variables.tf +93 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/main.tf +269 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/outputs.tf +23 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/variables.tf +50 -0
- package/dist/terraform/modules/data/s3-backups-bucket/main.tf +123 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +13 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +2 -2
- package/dist/terraform/modules/thinkwork/main.tf +439 -21
- package/dist/terraform/modules/thinkwork/outputs.tf +121 -0
- package/dist/terraform/modules/thinkwork/variables.tf +153 -2
- package/dist/terraform/schema.graphql +17 -0
- package/package.json +15 -14
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
################################################################################
|
|
11
11
|
|
|
12
12
|
locals {
|
|
13
|
-
create
|
|
14
|
-
use_aurora
|
|
15
|
-
use_rds
|
|
13
|
+
create = var.create_database
|
|
14
|
+
use_aurora = local.create && var.database_engine == "aurora-serverless"
|
|
15
|
+
use_rds = local.create && var.database_engine == "rds-postgres"
|
|
16
16
|
|
|
17
17
|
cluster_identifier = "thinkwork-${var.stage}-db"
|
|
18
18
|
master_username = "thinkwork_admin"
|
|
@@ -29,6 +29,16 @@ locals {
|
|
|
29
29
|
local.use_rds ? aws_db_instance.main[0].address : var.existing_db_endpoint
|
|
30
30
|
)
|
|
31
31
|
db_security_group_id = local.create ? aws_security_group.db[0].id : var.existing_db_security_group_id
|
|
32
|
+
|
|
33
|
+
# aws_s3 Aurora extension opts in when an Aurora cluster exists AND the
|
|
34
|
+
# caller set enable_aws_s3 = true. Plan-time gate is the explicit bool,
|
|
35
|
+
# not the backups_bucket_arn nullness — a freshly-created bucket's ARN
|
|
36
|
+
# is "known after apply," which broke count evaluation on greenfield
|
|
37
|
+
# deploys (see PR #526 for the incident). The ARN is still used inside
|
|
38
|
+
# the IAM policy body (jsonencode interpolates at apply time, fine).
|
|
39
|
+
# Non-aurora engines short-circuit before aws_rds_cluster.main[0] is
|
|
40
|
+
# dereferenced.
|
|
41
|
+
enable_aws_s3 = local.use_aurora && var.enable_aws_s3
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
data "aws_region" "current" {}
|
|
@@ -161,6 +171,97 @@ resource "aws_db_instance" "main" {
|
|
|
161
171
|
}
|
|
162
172
|
}
|
|
163
173
|
|
|
174
|
+
################################################################################
|
|
175
|
+
# aws_s3 extension IAM role (Aurora only; opt-in via backups_bucket_arn)
|
|
176
|
+
#
|
|
177
|
+
# Aurora Postgres ships with an `aws_s3` extension (CREATE EXTENSION IF NOT
|
|
178
|
+
# EXISTS aws_s3 CASCADE) that allows `aws_s3.query_export_to_s3(...)` to
|
|
179
|
+
# write query results directly to S3. It requires an IAM role associated
|
|
180
|
+
# with the cluster + trust for `rds.amazonaws.com` + `s3:PutObject`
|
|
181
|
+
# permission on the target bucket.
|
|
182
|
+
#
|
|
183
|
+
# When `backups_bucket_arn` is set, this block provisions the role, the
|
|
184
|
+
# policy, and the cluster association so that U5 of the thread-detail
|
|
185
|
+
# cleanup plan (packages/database-pg/drizzle/0027_thread_cleanup_drops.sql)
|
|
186
|
+
# can `SELECT aws_s3.query_export_to_s3(...)` without embedding any AWS
|
|
187
|
+
# credentials in the hand-rolled SQL.
|
|
188
|
+
#
|
|
189
|
+
# Post-deploy one-shot step (documented in the plan, not automated here):
|
|
190
|
+
# psql "$DATABASE_URL" -c "CREATE EXTENSION IF NOT EXISTS aws_s3 CASCADE"
|
|
191
|
+
################################################################################
|
|
192
|
+
|
|
193
|
+
resource "aws_iam_role" "aurora_aws_s3" {
|
|
194
|
+
count = local.enable_aws_s3 ? 1 : 0
|
|
195
|
+
|
|
196
|
+
name = "thinkwork-${var.stage}-aurora-aws-s3"
|
|
197
|
+
|
|
198
|
+
assume_role_policy = jsonencode({
|
|
199
|
+
Version = "2012-10-17"
|
|
200
|
+
Statement = [
|
|
201
|
+
{
|
|
202
|
+
Effect = "Allow"
|
|
203
|
+
Principal = { Service = "rds.amazonaws.com" }
|
|
204
|
+
Action = "sts:AssumeRole"
|
|
205
|
+
},
|
|
206
|
+
]
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
tags = {
|
|
210
|
+
Name = "thinkwork-${var.stage}-aurora-aws-s3"
|
|
211
|
+
Purpose = "aurora-aws_s3-extension"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
resource "aws_iam_role_policy" "aurora_aws_s3" {
|
|
216
|
+
count = local.enable_aws_s3 ? 1 : 0
|
|
217
|
+
|
|
218
|
+
name = "thinkwork-${var.stage}-aurora-aws-s3"
|
|
219
|
+
role = aws_iam_role.aurora_aws_s3[0].id
|
|
220
|
+
|
|
221
|
+
policy = jsonencode({
|
|
222
|
+
Version = "2012-10-17"
|
|
223
|
+
Statement = [
|
|
224
|
+
{
|
|
225
|
+
Sid = "PutBackupObjects"
|
|
226
|
+
Effect = "Allow"
|
|
227
|
+
Action = [
|
|
228
|
+
"s3:PutObject",
|
|
229
|
+
"s3:AbortMultipartUpload",
|
|
230
|
+
# GetBucketLocation is required by aws_s3.query_export_to_s3 for the
|
|
231
|
+
# region-matching check; without it exports fail at runtime with an
|
|
232
|
+
# opaque permission error. AWS Aurora docs:
|
|
233
|
+
# https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/postgresql-s3-export.html
|
|
234
|
+
"s3:GetBucketLocation",
|
|
235
|
+
]
|
|
236
|
+
# Object-level actions are scoped to pre-drop/* so this role can only
|
|
237
|
+
# write snapshots produced by destructive migrations, not read or
|
|
238
|
+
# overwrite arbitrary bucket content. GetBucketLocation applies to
|
|
239
|
+
# the bucket ARN itself (it is a bucket-level, not object-level,
|
|
240
|
+
# operation).
|
|
241
|
+
Resource = [
|
|
242
|
+
var.backups_bucket_arn,
|
|
243
|
+
"${var.backups_bucket_arn}/pre-drop/*",
|
|
244
|
+
]
|
|
245
|
+
},
|
|
246
|
+
]
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
resource "aws_rds_cluster_role_association" "aurora_aws_s3" {
|
|
251
|
+
count = local.enable_aws_s3 ? 1 : 0
|
|
252
|
+
|
|
253
|
+
db_cluster_identifier = aws_rds_cluster.main[0].id
|
|
254
|
+
feature_name = "s3Export"
|
|
255
|
+
role_arn = aws_iam_role.aurora_aws_s3[0].arn
|
|
256
|
+
|
|
257
|
+
# Force the inline policy to land before RDS validates the role. Terraform's
|
|
258
|
+
# implicit dependency graph only links this resource to the IAM role itself
|
|
259
|
+
# (via role_arn). RDS AddRoleToDBCluster verifies the trust policy + any
|
|
260
|
+
# attached inline policies server-side; applying the association before the
|
|
261
|
+
# policy has propagated can return AccessDenied on the first apply.
|
|
262
|
+
depends_on = [aws_iam_role_policy.aurora_aws_s3]
|
|
263
|
+
}
|
|
264
|
+
|
|
164
265
|
################################################################################
|
|
165
266
|
# Secrets Manager — DB Credentials (shared by both engines)
|
|
166
267
|
################################################################################
|
|
@@ -183,3 +284,63 @@ resource "aws_secretsmanager_secret_version" "db_credentials" {
|
|
|
183
284
|
password = var.db_password
|
|
184
285
|
})
|
|
185
286
|
}
|
|
287
|
+
|
|
288
|
+
################################################################################
|
|
289
|
+
# Secrets Manager — Compliance Role Credentials (Phase 3 U2)
|
|
290
|
+
#
|
|
291
|
+
# Three role-scoped secret containers for the compliance.* schema introduced
|
|
292
|
+
# in U1 (drizzle/0069_compliance_schema.sql, PR #880). Per Decision #4 of the
|
|
293
|
+
# master plan (docs/plans/2026-05-06-011-feat-compliance-audit-event-log-plan.md):
|
|
294
|
+
#
|
|
295
|
+
# - compliance/writer-credentials: used by Yoga resolvers + Lambda handlers
|
|
296
|
+
# via the U3 emitAuditEvent helper.
|
|
297
|
+
# - compliance/drainer-credentials: used by the U4 outbox drainer Lambda
|
|
298
|
+
# (reserved-concurrency=1).
|
|
299
|
+
# - compliance/reader-credentials: used by the graphql-http Lambda for
|
|
300
|
+
# U10 admin Compliance read paths.
|
|
301
|
+
#
|
|
302
|
+
# Naming follows the slash-delimited "thinkwork/${stage}/..." convention from
|
|
303
|
+
# CLAUDE.md (the master `db_credentials` secret above uses the grandfathered
|
|
304
|
+
# hyphen form). JSON shape is enriched vs the master's {username, password}:
|
|
305
|
+
# {username, password, host, port, dbname} so each consumer is self-contained.
|
|
306
|
+
#
|
|
307
|
+
# Greenfield bootstrap is operator-driven via scripts/bootstrap-compliance-roles.sh
|
|
308
|
+
# which reads passwords from env, populates these secrets via
|
|
309
|
+
# `aws secretsmanager put-secret-value`, and runs
|
|
310
|
+
# drizzle/0070_compliance_aurora_roles.sql to create the matching Aurora roles.
|
|
311
|
+
# Terraform owns the SECRET CONTAINER; the operator owns the SECRET VALUE.
|
|
312
|
+
#
|
|
313
|
+
# `lifecycle.ignore_changes = [secret_string]` lets operators rotate via the
|
|
314
|
+
# bootstrap script (or AWS console) without Terraform clobbering the value
|
|
315
|
+
# on the next apply.
|
|
316
|
+
################################################################################
|
|
317
|
+
|
|
318
|
+
resource "aws_secretsmanager_secret" "compliance_writer" {
|
|
319
|
+
count = local.create ? 1 : 0
|
|
320
|
+
name = "thinkwork/${var.stage}/compliance/writer-credentials"
|
|
321
|
+
|
|
322
|
+
tags = {
|
|
323
|
+
Name = "thinkwork-${var.stage}-compliance-writer-credentials"
|
|
324
|
+
Role = "compliance_writer"
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
resource "aws_secretsmanager_secret" "compliance_drainer" {
|
|
329
|
+
count = local.create ? 1 : 0
|
|
330
|
+
name = "thinkwork/${var.stage}/compliance/drainer-credentials"
|
|
331
|
+
|
|
332
|
+
tags = {
|
|
333
|
+
Name = "thinkwork-${var.stage}-compliance-drainer-credentials"
|
|
334
|
+
Role = "compliance_drainer"
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
resource "aws_secretsmanager_secret" "compliance_reader" {
|
|
339
|
+
count = local.create ? 1 : 0
|
|
340
|
+
name = "thinkwork/${var.stage}/compliance/reader-credentials"
|
|
341
|
+
|
|
342
|
+
tags = {
|
|
343
|
+
Name = "thinkwork-${var.stage}-compliance-reader-credentials"
|
|
344
|
+
Role = "compliance_reader"
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -28,3 +28,37 @@ output "database_engine" {
|
|
|
28
28
|
description = "Which engine is running (aurora-serverless or rds-postgres)"
|
|
29
29
|
value = var.database_engine
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
output "aws_s3_iam_role_arn" {
|
|
33
|
+
description = "ARN of the IAM role attached to the Aurora cluster for `aws_s3.query_export_to_s3` (only when backups_bucket_arn is set). Null otherwise. Useful for confirming the role attachment in post-deploy runbooks."
|
|
34
|
+
value = local.enable_aws_s3 ? aws_iam_role.aurora_aws_s3[0].arn : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# ----------------------------------------------------------------------------
|
|
38
|
+
# Compliance role secret ARNs (Phase 3 U2)
|
|
39
|
+
#
|
|
40
|
+
# Container-only — Terraform creates the AWS Secrets Manager resource;
|
|
41
|
+
# secret values are populated by scripts/bootstrap-compliance-roles.sh
|
|
42
|
+
# alongside the matching Aurora roles in
|
|
43
|
+
# drizzle/0070_compliance_aurora_roles.sql.
|
|
44
|
+
#
|
|
45
|
+
# Consumers:
|
|
46
|
+
# compliance_writer_secret_arn → U3 emitAuditEvent helper (resolver path)
|
|
47
|
+
# compliance_drainer_secret_arn → U4 outbox drainer Lambda
|
|
48
|
+
# compliance_reader_secret_arn → U10 graphql-http Compliance read path
|
|
49
|
+
# ----------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
output "compliance_writer_secret_arn" {
|
|
52
|
+
description = "Secrets Manager ARN for the compliance_writer Aurora role (Phase 3 U2). Empty string when create_database = false."
|
|
53
|
+
value = local.create ? aws_secretsmanager_secret.compliance_writer[0].arn : ""
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
output "compliance_drainer_secret_arn" {
|
|
57
|
+
description = "Secrets Manager ARN for the compliance_drainer Aurora role (Phase 3 U2). Empty string when create_database = false."
|
|
58
|
+
value = local.create ? aws_secretsmanager_secret.compliance_drainer[0].arn : ""
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
output "compliance_reader_secret_arn" {
|
|
62
|
+
description = "Secrets Manager ARN for the compliance_reader Aurora role (Phase 3 U2). Empty string when create_database = false."
|
|
63
|
+
value = local.create ? aws_secretsmanager_secret.compliance_reader[0].arn : ""
|
|
64
|
+
}
|
|
@@ -112,3 +112,19 @@ variable "deletion_protection" {
|
|
|
112
112
|
type = bool
|
|
113
113
|
default = null
|
|
114
114
|
}
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# aws_s3 Aurora extension (optional — set backups_bucket_arn to enable)
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
variable "backups_bucket_arn" {
|
|
121
|
+
description = "ARN of the S3 backups bucket Aurora should be allowed to write to via the aws_s3 extension (aws_s3.query_export_to_s3). Used as the Resource in the attached IAM policy; only takes effect when enable_aws_s3 = true. A not-yet-created bucket's ARN is 'known after apply' — gating on this nullness broke count at plan time, so use enable_aws_s3 as the plan-time gate instead."
|
|
122
|
+
type = string
|
|
123
|
+
default = null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
variable "enable_aws_s3" {
|
|
127
|
+
description = "Plan-time gate for the aws_s3 Aurora extension IAM role. Set true when Aurora should be allowed to PutObject into backups_bucket_arn. Only effective when database_engine = 'aurora-serverless'. Defaults to false to preserve the prior 'opt-in by passing backups_bucket_arn' shape when this variable is omitted."
|
|
128
|
+
type = bool
|
|
129
|
+
default = false
|
|
130
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# compliance-audit-bucket
|
|
2
|
+
|
|
3
|
+
WORM-protected S3 bucket for SOC2 Type 1 tamper-evident audit anchoring. Provisions the bucket that the anchor Lambda (U8a/U8b) writes Merkle-anchor evidence into, and ships the IAM role the Lambda will eventually assume.
|
|
4
|
+
|
|
5
|
+
Module path: `terraform/modules/data/compliance-audit-bucket/` (master-plan canonical name).
|
|
6
|
+
Bucket name: `thinkwork-${var.stage}-compliance-anchors` (master plan line 513 — plural, distinct from the module name).
|
|
7
|
+
|
|
8
|
+
## What this is
|
|
9
|
+
|
|
10
|
+
- An S3 bucket with Object Lock enabled at create time. Object Lock cannot be disabled after creation; this is a one-way commitment per AWS.
|
|
11
|
+
- Two prefixes:
|
|
12
|
+
- `anchors/` — Merkle-anchor objects. Subject to default retention. Lifecycle transitions to Glacier IR at 90 days. **No** expiration rule.
|
|
13
|
+
- `proofs/` — per-tenant proof slices written by the anchor Lambda. The bucket-level Lock applies, but U8b sets a shorter per-object retention because slices are derivable from the chain + anchor.
|
|
14
|
+
- A bucket policy denying any `s3:*` over plain HTTP and denying `s3:DeleteObject` / `s3:DeleteObjectVersion` from any principal — defense-in-depth on top of Object Lock.
|
|
15
|
+
- An IAM role (`thinkwork-${stage}-compliance-anchor-lambda-role`) that U8a's anchor Lambda will assume. The role's inline policy is path-scoped to `anchors/*` and `proofs/*`, grants only the actions the writer needs, and **explicitly denies** `s3:BypassGovernanceRetention` and `s3:PutObjectLegalHold` so the deny survives any future broadening of the role's IAM grants.
|
|
16
|
+
|
|
17
|
+
## Object Lock posture
|
|
18
|
+
|
|
19
|
+
| Stage | Default `mode` | Default retention | Notes |
|
|
20
|
+
|-------|----------------|-------------------|-------|
|
|
21
|
+
| dev / staging | `GOVERNANCE` | 365 days | Allows a privileged role with `s3:BypassGovernanceRetention` to delete or shorten retention. Required for dev iteration; the anchor Lambda role itself does **not** hold the bypass action (explicitly denied). |
|
|
22
|
+
| prod (audit-engagement time) | `COMPLIANCE` | 365 days | Irreversible — even AWS root cannot delete or shorten retention until it expires. |
|
|
23
|
+
|
|
24
|
+
### COMPLIANCE-mode cutover playbook (prod)
|
|
25
|
+
|
|
26
|
+
The flip is a one-line tfvars change in the prod stack at audit-engagement time:
|
|
27
|
+
|
|
28
|
+
```hcl
|
|
29
|
+
# terraform/examples/greenfield/terraform.tfvars (prod)
|
|
30
|
+
compliance_anchor_object_lock_mode = "COMPLIANCE"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
After `terraform apply`, verify with:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
aws s3api get-object-lock-configuration \
|
|
37
|
+
--bucket thinkwork-prod-compliance-anchors \
|
|
38
|
+
--query 'ObjectLockConfiguration.Rule.DefaultRetention.Mode'
|
|
39
|
+
# Expected: "COMPLIANCE"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Once flipped to COMPLIANCE, the bucket's default retention cannot be shortened by any principal, including AWS root. The flip itself is reversible by Terraform plan only until the first object is written; after that, AWS retains the lock state for the full retention window regardless of subsequent configuration changes.
|
|
43
|
+
|
|
44
|
+
## `force_destroy = false` invariant
|
|
45
|
+
|
|
46
|
+
`force_destroy` is hardcoded `false`. Object Lock + `force_destroy` interact pathologically:
|
|
47
|
+
|
|
48
|
+
- **COMPLIANCE mode:** `terraform destroy` cannot delete locked objects until retention expires (365 days). `force_destroy = true` would fail with `AccessDenied (403 Forbidden)` on every object.
|
|
49
|
+
- **GOVERNANCE mode:** `force_destroy = true` would only succeed if the deploying principal holds `s3:BypassGovernanceRetention`, which we explicitly do not grant. Granting it would defeat the audit-evidence posture.
|
|
50
|
+
|
|
51
|
+
Either way, `force_destroy = true` masks Object Lock retention behavior. Don't ship it.
|
|
52
|
+
|
|
53
|
+
### Dev cleanup playbook
|
|
54
|
+
|
|
55
|
+
> **U8b cutover note (2026-05-07):** the anchor Lambda now writes real WORM-locked
|
|
56
|
+
> bytes on every 15-minute cadence. A dev bucket that's been live for any non-trivial
|
|
57
|
+
> period accumulates objects under default 365-day retention — the dev stage's
|
|
58
|
+
> `GOVERNANCE` mode is the *only* thing that makes this playbook achievable. The
|
|
59
|
+
> `proofs/` prefix relies on the bucket-default lock; both anchor and proof objects
|
|
60
|
+
> require the bypass action below to delete. **Do not run this playbook against a
|
|
61
|
+
> COMPLIANCE-mode bucket** (prod) — the `s3:BypassGovernanceRetention` action is
|
|
62
|
+
> ineffective and the only recovery is rotating the bucket name on a fresh stage.
|
|
63
|
+
|
|
64
|
+
Tearing down a dev bucket requires admin-tier intervention (not a routine operator action) and is **not supported by `terraform destroy` alone**. The repo does not currently provision a break-glass role with `s3:BypassGovernanceRetention`; the operator performs the cleanup using their own admin credentials (or grants themselves the bypass action ad-hoc via an IAM policy attachment for the duration of the cleanup, then revokes it).
|
|
65
|
+
|
|
66
|
+
1. **Pre-requisite**: confirm your active credentials hold both `s3:BypassGovernanceRetention` AND `s3:DeleteObjectVersion` on the bucket. The anchor Lambda role itself **does not** hold these — it is explicitly Denied. If your admin role lacks them, attach a temporary inline policy:
|
|
67
|
+
```bash
|
|
68
|
+
aws iam put-role-policy --role-name <your-admin-role> \
|
|
69
|
+
--policy-name compliance-anchor-bypass-temp \
|
|
70
|
+
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:BypassGovernanceRetention","s3:DeleteObjectVersion","s3:DeleteObject"],"Resource":"arn:aws:s3:::thinkwork-dev-compliance-anchors/*"}]}'
|
|
71
|
+
```
|
|
72
|
+
2. Empty the bucket with the bypass flag (the bucket policy still denies `s3:DeleteObject` from any principal — see "Bucket-policy interaction" below before running this):
|
|
73
|
+
```bash
|
|
74
|
+
# First, you'll need to remove the DenyDeleteObject statement from the
|
|
75
|
+
# bucket policy temporarily. See Step 3 of "Bucket-policy interaction".
|
|
76
|
+
aws s3api list-object-versions --bucket thinkwork-dev-compliance-anchors \
|
|
77
|
+
--query '{Objects: Versions[].{Key: Key, VersionId: VersionId}}' \
|
|
78
|
+
| aws s3api delete-objects --bucket thinkwork-dev-compliance-anchors \
|
|
79
|
+
--bypass-governance-retention --delete file:///dev/stdin
|
|
80
|
+
# Repeat for delete markers if any (DeleteMarkers in list-object-versions output).
|
|
81
|
+
```
|
|
82
|
+
3. Delete the bucket:
|
|
83
|
+
```bash
|
|
84
|
+
aws s3api delete-bucket --bucket thinkwork-dev-compliance-anchors
|
|
85
|
+
```
|
|
86
|
+
4. Wait at least 1 hour before recreating with the same name (S3 bucket-name reuse latency).
|
|
87
|
+
5. If `terraform apply` is impatient, `terraform state rm module.compliance_anchors.aws_s3_bucket.anchor` to avoid the recreate-failure loop.
|
|
88
|
+
6. Revoke the temporary policy from step 1: `aws iam delete-role-policy --role-name <your-admin-role> --policy-name compliance-anchor-bypass-temp`.
|
|
89
|
+
|
|
90
|
+
#### Bucket-policy interaction
|
|
91
|
+
|
|
92
|
+
This module's bucket policy includes a `DenyDeleteObject` statement that applies to **all principals**, including admin roles with `s3:BypassGovernanceRetention`. To complete the dev cleanup, you must temporarily replace the bucket policy with one that omits the `DenyDeleteObject` statement (or attaches a Condition exempting your admin role):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# 1. Save the current policy
|
|
96
|
+
aws s3api get-bucket-policy --bucket thinkwork-dev-compliance-anchors \
|
|
97
|
+
--query Policy --output text > /tmp/anchor-bucket-policy-backup.json
|
|
98
|
+
|
|
99
|
+
# 2. Replace with a permissive policy (delete-allow only — keep EnforceHTTPS)
|
|
100
|
+
aws s3api put-bucket-policy --bucket thinkwork-dev-compliance-anchors \
|
|
101
|
+
--policy '{"Version":"2012-10-17","Statement":[{"Sid":"EnforceHTTPS","Effect":"Deny","Principal":"*","Action":"s3:*","Resource":["arn:aws:s3:::thinkwork-dev-compliance-anchors","arn:aws:s3:::thinkwork-dev-compliance-anchors/*"],"Condition":{"Bool":{"aws:SecureTransport":"false"}}}]}'
|
|
102
|
+
|
|
103
|
+
# 3. Now run the empty-bucket commands from the main playbook
|
|
104
|
+
# 4. After delete, the policy is gone with the bucket — no restore needed.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This playbook applies only to GOVERNANCE-mode buckets. COMPLIANCE-mode buckets cannot be emptied until retention expires — by design.
|
|
108
|
+
|
|
109
|
+
## KMS dependency
|
|
110
|
+
|
|
111
|
+
This module is **the org's first real consumer** of `module.kms.aws_kms_key.main` (the `alias/thinkwork-${stage}` CMK). The default key policy uses the standard "root account" statement, which permits any same-account principal whose IAM policy allows the action — the anchor Lambda role's inline policy fully covers this.
|
|
112
|
+
|
|
113
|
+
If a future PR tightens the KMS key policy without explicitly naming the anchor Lambda role, anchor writes will start failing with `KMSAccessDenied` at runtime — the failure mode is silent (no Terraform plan diff). The variable-level validation and resource-level `lifecycle.precondition` on the SSE configuration catch a missing `kms_key_arn` at plan time, but they cannot catch a key-policy regression. U8b's smoke test exercises the full Put/Get path and is the primary integration check.
|
|
114
|
+
|
|
115
|
+
## Provider pin
|
|
116
|
+
|
|
117
|
+
This module is compatible with the repo-wide `hashicorp/aws ~> 5.0` pin (locked to `5.100.0` in `terraform/examples/greenfield/.terraform.lock.hcl`). The `aws_s3_bucket_object_lock_configuration` standalone resource (current shape; not the deprecated inline `object_lock_configuration` block on `aws_s3_bucket`) has been stable since v4.0 (Feb 2022 split-resource refactor).
|
|
118
|
+
|
|
119
|
+
## Inputs
|
|
120
|
+
|
|
121
|
+
| Variable | Type | Default | Description |
|
|
122
|
+
|----------|------|---------|-------------|
|
|
123
|
+
| `stage` | string | _required_ | Deployment stage (e.g., `dev`, `prod`). Stages `prod` and `production` enforce `mode = "COMPLIANCE"` via Terraform `precondition`. |
|
|
124
|
+
| `account_id` | string | _required_ | AWS account ID. Used as `aws:SourceAccount` condition on the anchor Lambda role's trust policy (confused-deputy defense). |
|
|
125
|
+
| `bucket_name` | string | _required_ | Bucket name. Master-plan canonical: `thinkwork-${stage}-compliance-anchors`. |
|
|
126
|
+
| `kms_key_arn` | string | _required_ | CMK ARN for SSE-KMS. Wired from `module.kms.key_arn` at the composite root. Validated non-empty. |
|
|
127
|
+
| `mode` | string | `"GOVERNANCE"` | Object Lock retention mode. Validated ∈ {`GOVERNANCE`, `COMPLIANCE`}. Production stages reject `GOVERNANCE` via plan-time `precondition`. |
|
|
128
|
+
| `retention_days` | number | `365` | Default retention in days. Validated > 0. |
|
|
129
|
+
|
|
130
|
+
## Outputs
|
|
131
|
+
|
|
132
|
+
| Output | Description |
|
|
133
|
+
|--------|-------------|
|
|
134
|
+
| `bucket_name` | Bucket id (= `var.bucket_name`). |
|
|
135
|
+
| `bucket_arn` | Bucket ARN. |
|
|
136
|
+
| `lambda_role_arn` | IAM role ARN the anchor Lambda will assume (inert in U7 — U8a wires the function). |
|
|
137
|
+
|
|
138
|
+
## See also
|
|
139
|
+
|
|
140
|
+
- Master plan: `docs/plans/2026-05-06-011-feat-compliance-audit-event-log-plan.md` (U7 entry).
|
|
141
|
+
- U7 sub-plan: `docs/plans/2026-05-07-009-feat-compliance-u7-anchor-bucket-plan.md`.
|
|
142
|
+
- U8a sub-plan: `docs/plans/2026-05-07-010-feat-compliance-u8a-anchor-lambda-inert-plan.md` (inert seam).
|
|
143
|
+
- U8b sub-plan: `docs/plans/2026-05-07-012-feat-compliance-u8b-anchor-lambda-live-plan.md` (live S3 PutObject + Object Lock retention).
|
|
144
|
+
- AWS S3 Object Lock: <https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock-overview.html>.
|
|
145
|
+
- Cohasset Associates Compliance Assessment: <https://d1.awsstatic.com/r2018/b/S3-Object-Lock/Amazon-S3-Compliance-Assessment.pdf>.
|