carlos / platform Public

Merge remote-tracking branch 'origin/main' into sec-mail-user-boundary

# Conflicts:
#	internal/provision/testdata/plan14-root-reads-chain.tsv
claude agent, operated by paul@keymail.dev 60f767db97fc8f7eca566c78e5985e4909810fe5 parent 7f4eed6parent e3fd2e0
26 files changed, +684 −111
  • README.md +7 −0
  • cmd/console/main.go +54 −0
  • cmd/console/sidecaraccounts_serve_test.go +55 −0
  • cmd/console/sidecaraccounts_test.go +44 −0
  • docs/site/cli.md +6 −0
  • docs/superpowers/notes/census-proposal-console-brand-name.md +13 −0
  • docs/superpowers/notes/census-proposal-email-signin.md +4 −2
  • docs/superpowers/notes/census-proposal-sec-deps-dos-bumps.md +37 −0
  • docs/superpowers/notes/census-proposal-sec-host-imdsv2-module.md +28 −0
  • docs/superpowers/notes/census-proposal-sec-mail-user-boundary.md +2 −57
  • docs/superpowers/notes/census-proposal-sec-sidecar-create-gate.md +13 −0
  • go.mod +11 −12
  • go.sum +22 −34
  • infra/modules/region-host/main.tf +13 −0
  • infra/modules/region-host/tests/instance_metadata.tftest.hcl +49 −0
  • internal/console/apisidecars.go +13 −1
  • internal/console/server.go +7 −0
  • internal/console/sidecarsgate.go +64 −0
  • internal/console/sidecarsgate_test.go +193 −0
  • internal/provision/plan9_b8_census_test.go +1 −1
  • internal/provision/sec_sidecar_create_gate_census_test.go +27 −0
  • internal/provision/testdata/plan14-root-reads-chain.tsv +4 −1
  • internal/provision/testdata/plan14-root-reads-sec-deps-dos-bumps.tsv +3 −0
  • internal/provision/testdata/plan14-root-reads-sec-host-imdsv2-module.tsv +5 −0
  • internal/provision/testdata/plan14-root-reads-sec-mail-user-boundary.tsv +3 −3
  • internal/provision/testdata/plan14-root-reads-sec-sidecar-create-gate.tsv +6 −0

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
go: downloading github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.68.0
go: downloading github.com/aws/aws-sdk-go-v2 v1.44.0
go: downloading github.com/aws/aws-sdk-go-v2/service/costexplorer v1.67.5
go: downloading github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0
go: downloading github.com/aws/smithy-go v1.28.1
go: downloading github.com/superfly/ltx v0.5.1
go: downloading golang.org/x/net v0.60.0
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/config v1.32.6
go: downloading github.com/aws/aws-sdk-go-v2/service/sns v1.43.0
go: downloading github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1
go: downloading github.com/benbjohnson/litestream v0.5.14
go: downloading golang.org/x/crypto v0.57.0
go: downloading github.com/aws/aws-sdk-go-v2/credentials v1.19.6
go: downloading github.com/aws/aws-sdk-go-v2/service/sts v1.45.6
go: downloading golang.org/x/sys v0.48.0
go: downloading github.com/aws/aws-sdk-go-v2/service/acm v1.45.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/aws/aws-sdk-go-v2/service/iam v1.58.2
go: downloading github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.37.0
go: downloading github.com/oschwald/maxminddb-golang v1.13.1
go: downloading github.com/aws/aws-sdk-go-v2/service/autoscaling v1.74.0
go: downloading github.com/pion/logging v0.2.4
go: downloading github.com/pion/stun/v3 v3.1.5
go: downloading github.com/aws/aws-sdk-go-v2/service/sesv2 v1.66.6
go: downloading github.com/pion/turn/v4 v4.1.4
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading golang.org/x/crypto v0.57.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40
go: downloading github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40
go: downloading github.com/prometheus/client_golang v1.17.0
go: downloading golang.org/x/sync v0.23.0
go: downloading github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18
go: downloading github.com/aws/aws-sdk-go-v2/service/autoscaling v1.74.0
go: downloading github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/klauspost/compress v1.18.0
go: downloading github.com/pion/logging v0.2.4
go: downloading github.com/pion/stun/v3 v3.1.5
go: downloading github.com/lmittmann/tint v1.1.3
go: downloading github.com/pion/turn/v4 v4.1.4
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/aws/aws-sdk-go-v2/service/sesv2 v1.66.6
go: downloading github.com/pion/transport/v4 v4.0.2
go: downloading github.com/pion/dtls/v3 v3.1.4
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23
go: downloading github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/service/signin v1.0.4
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12
go: downloading github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4
go: downloading golang.org/x/text v0.42.0
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19
go: downloading github.com/aws/aws-sdk-go-v2/service/signin v1.0.4
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12
go: downloading golang.org/x/text v0.42.0
go: downloading github.com/wlynxg/anet v0.0.5
go: downloading github.com/wlynxg/anet v0.0.5
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/prometheus/common v0.45.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading github.com/prometheus/common v0.45.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/pion/randutil v0.1.0
go: downloading github.com/pion/randutil v0.1.0
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/google/uuid v1.6.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
=== launchctx (4 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (46 s)
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/aws/aws-sdk-go-v2/service/autoscaling v1.74.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
go: downloading github.com/klauspost/compress v1.18.0
go: downloading github.com/pion/logging v0.2.4
go: downloading github.com/pion/turn/v4 v4.1.4
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19
go: downloading github.com/pion/dtls/v3 v3.1.4
go: downloading github.com/pion/transport/v4 v4.0.2
go: downloading golang.org/x/tools v0.49.0
go: downloading github.com/aws/aws-sdk-go-v2/service/signin v1.0.4
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12
go: downloading golang.org/x/text v0.42.0
go: downloading github.com/wlynxg/anet v0.0.5
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/pion/randutil v0.1.0
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading golang.org/x/mod v0.41.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/google/uuid v1.6.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
=== gofmt (1 s)
gofmt -l .: clean
=== rest (199 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.330s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.841s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.689s
ok  	github.com/carlosframework/platform/internal/activator	58.957s
ok  	github.com/carlosframework/platform/internal/agentsock	0.027s
ok  	github.com/carlosframework/platform/internal/alerting	0.008s
ok  	github.com/carlosframework/platform/internal/analytics	0.023s
ok  	github.com/carlosframework/platform/internal/appconfig	2.587s
ok  	github.com/carlosframework/platform/internal/apperr	0.119s
ok  	github.com/carlosframework/platform/internal/applog	0.272s
ok  	github.com/carlosframework/platform/internal/awsinv	0.008s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.006s
ok  	github.com/carlosframework/platform/internal/boxid	0.819s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.008s
ok  	github.com/carlosframework/platform/internal/caps	0.015s
ok  	github.com/carlosframework/platform/internal/certwarm	0.005s
ok  	github.com/carlosframework/platform/internal/cgroup	0.758s
ok  	github.com/carlosframework/platform/internal/changelog	0.467s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.084s
ok  	github.com/carlosframework/platform/internal/clamd	0.059s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	7.827s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.015s
ok  	github.com/carlosframework/platform/internal/dbquery	0.011s
ok  	github.com/carlosframework/platform/internal/dbreads	2.143s
?   	github.com/carlosframework/platform/internal/dbreads/dbreadstest	[no test files]
ok  	github.com/carlosframework/platform/internal/dbverify	0.008s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.005s
ok  	github.com/carlosframework/platform/internal/docsite	0.011s
ok  	github.com/carlosframework/platform/internal/domains	2.596s
ok  	github.com/carlosframework/platform/internal/doorkey	0.012s
ok  	github.com/carlosframework/platform/internal/economics	0.010s
ok  	github.com/carlosframework/platform/internal/edge	49.598s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.138s
ok  	github.com/carlosframework/platform/internal/gate	1.656s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.012s
ok  	github.com/carlosframework/platform/internal/geoip	0.029s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.155s
ok  	github.com/carlosframework/platform/internal/host	146.915s
ok  	github.com/carlosframework/platform/internal/hostactors	6.374s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.005s
ok  	github.com/carlosframework/platform/internal/hostbox	3.263s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.751s
ok  	github.com/carlosframework/platform/internal/hostenv	0.005s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.010s
ok  	github.com/carlosframework/platform/internal/hostexec	5.714s
ok  	github.com/carlosframework/platform/internal/hostfence	0.037s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.441s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.019s
ok  	github.com/carlosframework/platform/internal/hostguard	1.037s
ok  	github.com/carlosframework/platform/internal/hostops	2.019s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.003s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.004s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.640s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.202s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.002s
ok  	github.com/carlosframework/platform/internal/infraview	0.003s
ok  	github.com/carlosframework/platform/internal/instancectl	5.249s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.835s
ok  	github.com/carlosframework/platform/internal/instancefs	0.015s
ok  	github.com/carlosframework/platform/internal/instancekey	0.004s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.013s
ok  	github.com/carlosframework/platform/internal/layout	1.800s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.004s
ok  	github.com/carlosframework/platform/internal/lease	10.096s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.623s
ok  	github.com/carlosframework/platform/internal/ledger	0.030s
ok  	github.com/carlosframework/platform/internal/mail	0.220s
ok  	github.com/carlosframework/platform/internal/maintenance	0.107s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.570s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.117s
ok  	github.com/carlosframework/platform/internal/metrics	0.006s
ok  	github.com/carlosframework/platform/internal/moves	29.744s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.030s
ok  	github.com/carlosframework/platform/internal/objstore	0.322s
ok  	github.com/carlosframework/platform/internal/ops	0.239s
ok  	github.com/carlosframework/platform/internal/park	0.258s
ok  	github.com/carlosframework/platform/internal/plans	0.008s
ok  	github.com/carlosframework/platform/internal/privatefile	0.007s
ok  	github.com/carlosframework/platform/internal/provision	51.348s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.013s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.977s
ok  	github.com/carlosframework/platform/internal/relay	7.203s
ok  	github.com/carlosframework/platform/internal/release	34.202s
ok  	github.com/carlosframework/platform/internal/rollout	4.195s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.009s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.062s
ok  	github.com/carlosframework/platform/internal/rootstate	0.035s
ok  	github.com/carlosframework/platform/internal/runs	0.593s
ok  	github.com/carlosframework/platform/internal/s3compat	0.008s
ok  	github.com/carlosframework/platform/internal/safefile	0.009s
ok  	github.com/carlosframework/platform/internal/schedule	2.837s
ok  	github.com/carlosframework/platform/internal/seeds	0.014s
ok  	github.com/carlosframework/platform/internal/sim/engines	2.938s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.661s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.600s
ok  	github.com/carlosframework/platform/internal/status	0.067s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.304s
ok  	github.com/carlosframework/platform/internal/svcrole	0.003s
ok  	github.com/carlosframework/platform/internal/telemetry	0.005s
ok  	github.com/carlosframework/platform/internal/termfence	0.007s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.004s
ok  	github.com/carlosframework/platform/internal/updatewords	0.005s
ok  	github.com/carlosframework/platform/internal/usage	0.006s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.709s
=== internal_console@0 (83 s)
ok  	github.com/carlosframework/platform/internal/console	81.312s
=== internal_console@1 (50 s)
ok  	github.com/carlosframework/platform/internal/console	47.971s
=== internal_console@2 (44 s)
ok  	github.com/carlosframework/platform/internal/console	42.302s
=== internal_console@3 (46 s)
ok  	github.com/carlosframework/platform/internal/console	44.829s
=== internal_console@4 (47 s)
ok  	github.com/carlosframework/platform/internal/console	45.829s
=== internal_console@5 (48 s)
ok  	github.com/carlosframework/platform/internal/console	46.636s
=== internal_console@6 (45 s)
ok  	github.com/carlosframework/platform/internal/console	43.553s
=== internal_console@7 (52 s)
ok  	github.com/carlosframework/platform/internal/console	49.988s
=== root@0 (112 s)
ok  	github.com/carlosframework/platform	110.462s
=== root@1 (84 s)
ok  	github.com/carlosframework/platform	82.923s
=== root@2 (96 s)
ok  	github.com/carlosframework/platform	94.435s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 50659490 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.026s
=== RUN   TestT4StorageProbeMinio
2026/10/24 21:16:13 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E193BE81D507E2, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.022s
=== RUN   TestS3StoreMinioContract
=== RUN   TestS3StoreMinioContract/conditional_get_and_etag_listing
=== RUN   TestS3StoreMinioContract/bump_through_every_mutation_path
--- PASS: TestS3StoreMinioContract (0.03s)
    --- PASS: TestS3StoreMinioContract/conditional_get_and_etag_listing (0.01s)
    --- PASS: TestS3StoreMinioContract/bump_through_every_mutation_path (0.02s)
PASS
ok  	github.com/carlosframework/platform/internal/release	0.031s
Bucket created successfully `local/dbf-deploy`.
Bucket created successfully `local/dbf-data-ie`.
Bucket created successfully `local/dbf-data-uk`.
local/dbf-deploy versioning is enabled
Created policy `dbf-console` successfully.
Created policy `dbf-ukbox` successfully.
Added user `dbfconsole` successfully.
Added user `dbfukbox` successfully.
Attached Policies: [dbf-console]
To User: dbfconsole
Attached Policies: [dbf-ukbox]
To User: dbfukbox
=== RUN   TestDBForwardMinio
2026/10/24 21:16:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 21:16:15 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 21:16:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 21:16:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 21:16:15 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 21:16:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 21:16:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000002
2026/10/24 21:16:15 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000002 maxTXID=0000000000000002 size=370
2026/10/24 21:16:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000002
2026/10/24 21:16:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000003
2026/10/24 21:16:15 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000003 maxTXID=0000000000000003 size=454
2026/10/24 21:16:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000003 txid.db=0000000000000003
2026/10/24 21:16:15 console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically
2026/10/24 21:16:15 console: could not notify member@example.com of a CLI token issue: keymail: lookup member@example.com: keymail: GET /api/federation/lookup?addr=member%40example.com&from=console%40127.0.0.1%3A43381: could not determine target mailbox
    agent.go:135: dbreads: dbr_r4ljijgxbxt7denspmsfmgmq6q: planned 3 files (1544 bytes) and restored to 3 in 10ms
    agent.go:135: dbreads: dbr_r4ljijgxbxt7denspmsfmgmq6q: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 21:16:16 console: db read dbr_r4ljijgxbxt7denspmsfmgmq6q: relayed 1 chunks (102 bytes) in 1.003s, ended done
    agent.go:135: dbreads: dbr_vhqrkpbmkxhrway4irljmpv3na: planned 1 files (720 bytes) and restored to 1 in 8ms
    agent.go:135: dbreads: dbr_vhqrkpbmkxhrway4irljmpv3na: output 5 bytes in 1 confirmed chunks in 2ms
2026/10/24 21:16:17 console: db read dbr_vhqrkpbmkxhrway4irljmpv3na: relayed 1 chunks (102 bytes) in 1.003s, ended done
    agent.go:135: dbreads: dbr_xqcteayj6kn3d63irgh7jh6cfq: planned 3 files (1544 bytes) and held through 3 in 9ms
    agent.go:135: dbreads: dbr_xqcteayj6kn3d63irgh7jh6cfq: output 1544 bytes in 1 confirmed chunks in 1ms
2026/10/24 21:16:18 console: db read dbr_xqcteayj6kn3d63irgh7jh6cfq: relayed 1 chunks (1641 bytes) in 1.004s, ended done
=== RUN   TestDBForwardMinio/by_disconnect
=== NAME  TestDBForwardMinio
    agent.go:135: dbreads: dbr_eb4bcuniiw72pmiffgo4wvgf6a: planned 3 files (1544 bytes) and restored to 3 in 9ms
2026/10/24 21:16:18 console: db read dbr_eb4bcuniiw72pmiffgo4wvgf6a: relayed 0 chunks (0 bytes) in 23ms, ended client gone
    agent.go:135: dbreads: dbr_eb4bcuniiw72pmiffgo4wvgf6a: output 5 bytes in 1 confirmed chunks in 104ms
=== RUN   TestDBForwardMinio/by_timeout
=== NAME  TestDBForwardMinio
    agent.go:135: dbreads: dbr_tz3dxlyaon7zbowfxi32ocxfza: planned 3 files (1544 bytes) and restored to 3 in 9ms
2026/10/24 21:16:38 console: db read dbr_tz3dxlyaon7zbowfxi32ocxfza: relayed 0 chunks (0 bytes) in 19.148s, ended no-result
    agent.go:135: dbreads: dbr_tz3dxlyaon7zbowfxi32ocxfza: output 5 bytes in 1 confirmed chunks in 19.123s
--- PASS: TestDBForwardMinio (22.58s)
    --- PASS: TestDBForwardMinio/by_disconnect (0.22s)
    --- PASS: TestDBForwardMinio/by_timeout (19.26s)
PASS
ok  	github.com/carlosframework/platform/internal/console	22.597s
minio contract: PASS
diff --git a/README.md b/README.md
index 14c9947fa..3cf801394 100644
--- a/README.md
+++ b/README.md
@@ -660,6 +660,13 @@ publish an inflated instance count.
`carlos system geoip publish` instead (docs/site/cli.md, "Platform GeoIP").
Uploads are staged in `geoip-staging/` next to `CONSOLE_DB`, which the
console clears at startup.
+- `CARLOS_SIDECAR_ACCOUNTS`: comma-separated account sqids (not names) that
+ may still enable and create native sidecars. Set, every other account gets
+ 403 `sidecars-paused` from `carlos sidecars enable` and `carlos sidecars
+ create`. List, get, wake and delete still work, and existing sidecars keep
+ running. Native sidecars run on the flat tier, outside the nested tenants'
+ isolation, until nested sidecars land (discussion #370). **Unset or empty =
+ no gate.** An entry that is not a sqid stops the console at boot.
**Canonical-host caveat**, stated honestly: an app that pins its own host
(e.g. keymail's `--host keymail.dev`) will redirect or 404 on its platform
diff --git a/cmd/console/main.go b/cmd/console/main.go
index e3b30551f..d09b991b3 100644
--- a/cmd/console/main.go
+++ b/cmd/console/main.go
@@ -51,6 +51,14 @@
// CARLOS_TEMPORARY_APPS=1 turns on temporary apps (carlos deploy
// --temporary); off by default, see the temporary static sites rollout.
//
+// CARLOS_SIDECAR_ACCOUNTS (comma-separated account sqids) pauses native
+// sidecars for every account it does not name: enabling sidecars and
+// creating one answer 403 sidecars-paused, while list, get, wake and delete
+// keep working and existing sidecars keep running. Native sidecars run on
+// the flat tier, outside the nested tenants' isolation, until discussion
+// #370 lands. Unset or empty is no gate. An entry that is not a sqid, or a
+// value that names no sqid at all, stops the console at startup.
+//
// Tenant email (task 17) reads CARLOS_MAIL_OPERATORS (comma-separated
// addresses; narrows the fleet-operator bit further for the pause/cap
// door only — see mailOperators' doc comment), CARLOS_MAIL_REGION,
@@ -133,6 +141,7 @@ import (
"github.com/carlosframework/platform/internal/appconfig"
"github.com/carlosframework/platform/internal/console"
"github.com/carlosframework/platform/internal/dnsrec"
+ "github.com/carlosframework/platform/internal/domains"
"github.com/carlosframework/platform/internal/geo"
"github.com/carlosframework/platform/internal/geoip"
"github.com/carlosframework/platform/internal/lease"
@@ -317,6 +326,12 @@ func serve() {
} else {
log.Print("console: CARLOS_RELEASE_KEY unset — promotions disabled, running read-only")
}
+ // Before anything is opened: a typo in the allowlist must stop the
+ // console rather than quietly pause, or unpause, the wrong accounts.
+ sidecarAccts, err := sidecarAccounts(os.Getenv)
+ if err != nil {
+ log.Fatalf("console: %v", err)
+ }
hostClaim := hostClaimEnabled(os.Getenv)
st := openServeDB(hostClaim)
storeProvisioner, storeDeployment := storeBackend()
@@ -490,6 +505,9 @@ func serve() {
// owners (discussion #176). Unset is the owner-only console.
SystemOperators: systemOperators(),
PlatformDomain: platformDomain(),
+ // The sidecar pause (internal/console/sidecarsgate.go). Unset is no
+ // gate; set, only these accounts may enable or create a sidecar.
+ SidecarAccounts: sidecarAccts,
}
// The Phase 1 moves switch (CARLOS_MOVES_PHASE1, off by default, index
// D13): applied after the literal so a test can prove it reaches the
@@ -919,6 +937,42 @@ func systemOperators() []string {
return out
}
+// sidecarAccounts parses CARLOS_SIDECAR_ACCOUNTS (comma-separated account
+// sqids): the accounts that may still enable and create native sidecars
+// while every other account is paused (internal/console/sidecarsgate.go,
+// discussion #370). Each entry is trimmed and lower-cased, and must be a
+// sqid (domains.ValidSqid), never an account name: "ops" is not one, and a
+// name that matched nothing would pause the very account it meant to keep.
+// An invalid entry is an error for serve to stop on.
+//
+// Unset or the empty string is nil, and nil is no gate: every account may
+// enable and create sidecars, exactly as before the list existed. This is
+// the opposite direction to the operator lists above on purpose, so a
+// deployment that never sets it is unaffected. Any other value that names
+// no sqid at all ("," or " , ") is an error too: it reads as a list, and
+// parsing it to nil would quietly turn the gate off.
+func sidecarAccounts(getenv func(string) string) ([]string, error) {
+ raw := getenv("CARLOS_SIDECAR_ACCOUNTS")
+ if raw == "" {
+ return nil, nil
+ }
+ var out []string
+ for _, a := range strings.Split(raw, ",") {
+ a = strings.ToLower(strings.TrimSpace(a))
+ if a == "" {
+ continue
+ }
+ if !domains.ValidSqid(a) {
+ return nil, fmt.Errorf("CARLOS_SIDECAR_ACCOUNTS: %q is not an account sqid (the account's id, e.g. bab, never its name)", a)
+ }
+ out = append(out, a)
+ }
+ if len(out) == 0 {
+ return nil, fmt.Errorf("CARLOS_SIDECAR_ACCOUNTS: %q names no account sqid; unset it for no gate, or list at least one sqid", raw)
+ }
+ return out, nil
+}
+
// platformDomain reads CARLOS_PLATFORM_DOMAIN — the domain the relay's own
// hostnames hang off (relay.<region>.<domain>), which an enabled app is
// handed as its ICE server URLs.
diff --git a/cmd/console/sidecaraccounts_serve_test.go b/cmd/console/sidecaraccounts_serve_test.go
new file mode 100644
index 000000000..066138fba
--- /dev/null
+++ b/cmd/console/sidecaraccounts_serve_test.go
@@ -0,0 +1,55 @@
+package main_test
+
+import (
+ "context"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+)
+
+// TestServeStopsOnABadSidecarAccount: serve reads CARLOS_SIDECAR_ACCOUNTS
+// and stops before it opens anything on an entry that is not a sqid, or on a
+// value set to a list that names no sqid at all ("," would otherwise parse
+// to nil and quietly turn the gate off). The timeout is the failure mode it
+// guards: a serve that ignored the variable would carry on and listen.
+func TestServeStopsOnABadSidecarAccount(t *testing.T) {
+ bin := buildConsole(t)
+ for _, c := range []struct{ val, want string }{
+ {"bab,ops", `"ops"`},
+ {",", "names no account sqid"},
+ {" , ", "names no account sqid"},
+ } {
+ tmp := t.TempDir()
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ cmd := exec.CommandContext(ctx, bin, "serve")
+ cmd.Dir = tmp
+ cmd.Env = append(os.Environ(),
+ "CONSOLE_ORIGIN=http://127.0.0.1:1",
+ "CONSOLE_ADDR=127.0.0.1:0",
+ "CONSOLE_DB="+filepath.Join(tmp, "console.db"),
+ // 32 zero bytes: a well-formed Ed25519 public key, so serve gets
+ // past the key check to the variable under test.
+ "CARLOS_RELEASE_PUBKEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+ "CARLOS_RELEASE_KEY=",
+ "CARLOS_SIDECAR_ACCOUNTS="+c.val,
+ )
+ out, err := cmd.CombinedOutput()
+ timedOut := ctx.Err() != nil
+ cancel()
+ if timedOut {
+ t.Fatalf("CARLOS_SIDECAR_ACCOUNTS=%q: serve did not stop: %s", c.val, out)
+ }
+ if err == nil {
+ t.Fatalf("CARLOS_SIDECAR_ACCOUNTS=%q: serve exited 0: %s", c.val, out)
+ }
+ if !strings.Contains(string(out), "CARLOS_SIDECAR_ACCOUNTS") || !strings.Contains(string(out), c.want) {
+ t.Fatalf("CARLOS_SIDECAR_ACCOUNTS=%q: the refusal does not name the variable and %s: %s", c.val, c.want, out)
+ }
+ if _, err := os.Stat(filepath.Join(tmp, "console.db")); err == nil {
+ t.Fatalf("CARLOS_SIDECAR_ACCOUNTS=%q: serve opened its database before refusing", c.val)
+ }
+ }
+}
diff --git a/cmd/console/sidecaraccounts_test.go b/cmd/console/sidecaraccounts_test.go
new file mode 100644
index 000000000..cba72c2bd
--- /dev/null
+++ b/cmd/console/sidecaraccounts_test.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "slices"
+ "testing"
+)
+
+// CARLOS_SIDECAR_ACCOUNTS reaches Config.SidecarAccounts through
+// sidecarAccounts: trimmed, lower-cased, each a sqid. Unset and the empty
+// string are nil, which is no gate; any entry that is not a sqid, and any
+// other value that names no sqid, is an error serve stops on (TestServeStopsOnABadSidecarAccount runs that through the binary).
+func TestSidecarAccountsParsesTheList(t *testing.T) {
+ for _, tc := range []struct {
+ val string
+ want []string
+ wantErr bool
+ }{
+ {"", nil, false},
+ {" ", nil, true}, // set, but names no sqid: refused, never "no gate"
+ {",", nil, true}, // the same, and the review's case
+ {" , ", nil, true}, // ditto
+ {"bab", []string{"bab"}, false},
+ {" BAB , cdef ,", []string{"bab", "cdef"}, false},
+ {"bab,ops", nil, true}, // a name, not a sqid: o is not in the alphabet
+ {"bab,ab", nil, true}, // too short
+ {"bab,b-b", nil, true}, // not a letter
+ {"bab;cdef", nil, true}, // wrong separator reads as one bad entry
+ {"x1234567890abcdefg", nil, true},
+ } {
+ got, err := sidecarAccounts(func(k string) string {
+ if k != "CARLOS_SIDECAR_ACCOUNTS" {
+ t.Fatalf("read %s", k)
+ }
+ return tc.val
+ })
+ if (err != nil) != tc.wantErr {
+ t.Errorf("CARLOS_SIDECAR_ACCOUNTS=%q: err %v, want error %v", tc.val, err, tc.wantErr)
+ continue
+ }
+ if !slices.Equal(got, tc.want) {
+ t.Errorf("CARLOS_SIDECAR_ACCOUNTS=%q: %v, want %v", tc.val, got, tc.want)
+ }
+ }
+}
diff --git a/docs/site/cli.md b/docs/site/cli.md
index 0a8e384f2..3d0dd287a 100644
--- a/docs/site/cli.md
+++ b/docs/site/cli.md
@@ -1016,6 +1016,12 @@ it back from, so `carlos sidecars ls` shows the build its box last started
for each sidecar, beside the version its channel points at. One that has not
started on any box reads as unknown rather than as current.
+A console operator can pause sidecars for every account not named in
+`CARLOS_SIDECAR_ACCOUNTS`, a comma-separated list of account sqids. A paused
+account gets a 403 with the code `sidecars-paused` from `enable` and
+`create`. Its existing sidecars keep running, and `ls`, `wake` and `rm` still
+work. Unset, nothing is paused.
+
### carlos steering
Decides whether one instance's DNS answer varies by where the request comes
diff --git a/docs/superpowers/notes/census-proposal-console-brand-name.md b/docs/superpowers/notes/census-proposal-console-brand-name.md
new file mode 100644
index 000000000..e2e8ccd7d
--- /dev/null
+++ b/docs/superpowers/notes/census-proposal-console-brand-name.md
@@ -0,0 +1,13 @@
+# Census: console brand name
+
+Status: APPROVED by Paul (census owner), 2026-10-24, recorded on #280 with
+main at 65ca4aca. Rows keep `rev3=PROPOSED-console-brand-name` (no flip under
+#369).
+
+Overlay `internal/provision/testdata/plan14-root-reads-console-brand-name.tsv`,
+landed with 5f341f3. The console's chrome and new-account welcome name the
+deployment from `CARLOS_CONSOLE_NAME`. Three embedded templates change text
+only (`apps.html`, `home.html`, `layout.html`), and the `internal/console` and
+`internal/console/templates` envelopes are re-pinned. No read, file or record
+write is added. The units run in the console (`cmd/console serve` under
+`console.service`, user carlos-console).
diff --git a/docs/superpowers/notes/census-proposal-email-signin.md b/docs/superpowers/notes/census-proposal-email-signin.md
index f1f186c70..c32d7eb3a 100644
--- a/docs/superpowers/notes/census-proposal-email-signin.md
+++ b/docs/superpowers/notes/census-proposal-email-signin.md
@@ -1,7 +1,9 @@
# Census: console email sign-in
-Status: PROPOSED. Every row carries `rev3=PROPOSED-email-signin`. The ruling
-is asked of the census owner on #280.
+Status: APPROVED by Paul (census owner), 2026-10-24, recorded on #280 with
+main at 65ca4aca. The rows keep `rev3=PROPOSED-email-signin`: under the #369
+process nothing is flipped, and later overlays re-froze units at these
+dispositions, so a flip would move their fingerprints.
Branch `email-signin`, cut from main at 4d6f0e6c. Design:
`docs/superpowers/specs/2026-10-21-console-email-signin-design.md`.
diff --git a/docs/superpowers/notes/census-proposal-sec-deps-dos-bumps.md b/docs/superpowers/notes/census-proposal-sec-deps-dos-bumps.md
new file mode 100644
index 000000000..97d2e1f04
--- /dev/null
+++ b/docs/superpowers/notes/census-proposal-sec-deps-dos-bumps.md
@@ -0,0 +1,37 @@
+# Census approval: sec-deps-dos-bumps
+
+Ruling `census-ruling-sec-deps-dos-bumps`. Every change below keeps the disposition it already has, so it needs an approval, not a decision.
+
+Envelopes re-pinned: `.`.
+
+Seal: `945934f5978ace14dc5a9bae0ed69dfabf29b994504fc843c03d4a633ecd6c6a` → `945934f5978ace14dc5a9bae0ed69dfabf29b994504fc843c03d4a633ecd6c6a` (unchanged; loaded after sec-host-imdsv2-module, re-chained 2026-10-24 when that merged as ef156d3).
+
+Overlay: `internal/provision/testdata/plan14-root-reads-sec-deps-dos-bumps.tsv`, appended to `internal/provision/testdata/plan14-root-reads-chain.tsv`.
+
+## Branch context
+
+Branch `sec-deps-dos-bumps` bumps three modules that govulncheck reports as
+reachable on main:
+
+| module | main | branch | findings |
+|---|---|---|---|
+| `golang.org/x/net` | v0.56.0 | v0.60.0 | GO-2026-6603, -6610, -6611, -6612, -6617 (HTTP/2 server and transport DoS) |
+| `golang.org/x/text` | v0.38.0 | v0.42.0 | GO-2026-5970 (idna infinite loop, reached from the edge's TLS SNI handling) |
+| `github.com/pion/stun/v3` | v3.0.1 | v3.1.5 | GO-2026-6163 (panic on a malformed XOR-MAPPED-ADDRESS, reached from the relay's TURN server) |
+
+x/text lands at v0.42.0, not v0.39.0, because x/net v0.60.0 requires it. The
+same requirement moves x/crypto (v0.57.0), x/sys (v0.48.0), x/sync (v0.23.0),
+x/mod (v0.41.0) and x/tools (v0.49.0), and raises go.mod's `go` directive from
+`1.26` to `1.26.0`. stun v3.1.5 moves pion/dtls/v3 to v3.1.4 and
+pion/transport/v4 to v4.0.2, and drops pion/transport/v3. pion/turn/v4 stays at
+v4.1.4.
+
+The change is go.mod and go.sum only. No Go source file changes, so no read is
+added, removed or changed, no census unit's digest moves, and the legacy seal
+does not move. The only census effect is the envelope re-pin of `.` above.
+The record-write census (9B8) and claim routes are unchanged.
+
+go.mod meets `go-1.26.10-emudhra`, which raises the same `go` directive to
+`1.26.10` and pins `.` in its own overlay. Whichever lands second takes
+`1.26.10` and re-pins `.` once more (rerun `make census-propose` with the same
+NAME).
diff --git a/docs/superpowers/notes/census-proposal-sec-host-imdsv2-module.md b/docs/superpowers/notes/census-proposal-sec-host-imdsv2-module.md
new file mode 100644
index 000000000..37512514f
--- /dev/null
+++ b/docs/superpowers/notes/census-proposal-sec-host-imdsv2-module.md
@@ -0,0 +1,28 @@
+# Census approval: sec-host-imdsv2-module
+
+Ruling `census-ruling-sec-host-imdsv2-module`. Every change below keeps the disposition it already has, so it needs an approval, not a decision.
+
+| unit | change | read sites | what changed |
+|---|---|---|---|
+| `infra/modules/region-host/main.tf` `external` | body | unchanged (1) | aws_instance.host gains metadata_options (IMDSv2 required, hop limit 1, endpoint enabled), matching scanner-host; tofu configuration only, run on an operator's machine; no read, file or record write is added; applies in place on a running instance with no restart |
+
+Envelopes re-pinned: `infra/modules/region-host`.
+
+Seal: `09e20ea4bbc534710334e632e903878656dabfb5817d4d96c6e7c56508172148` → `945934f5978ace14dc5a9bae0ed69dfabf29b994504fc843c03d4a633ecd6c6a`.
+
+Overlay: `internal/provision/testdata/plan14-root-reads-sec-host-imdsv2-module.tsv`, appended to `internal/provision/testdata/plan14-root-reads-chain.tsv`.
+
+## Branch context
+
+The branch `sec-host-imdsv2-module` adds a `metadata_options` block to
+`aws_instance.host` in `infra/modules/region-host/main.tf` (IMDSv2 required,
+hop limit 1, endpoint enabled), matching scanner-host, and a plan-time tofu
+test under `infra/modules/region-host/tests/`. The record-write census does
+not change: no record write is added or moved.
+
+The new `tests/instance_metadata.tftest.hcl` is outside the census: the scan's
+extension list excludes `.tftest.hcl`, and envelopes group by immediate
+directory, so the module hash covers only the `.tf` files.
+
+Executing identity: tofu on an operator's machine with the operator's AWS
+credentials; no box unit runs the file and no binary embeds it.
diff --git a/docs/superpowers/notes/census-proposal-sec-mail-user-boundary.md b/docs/superpowers/notes/census-proposal-sec-mail-user-boundary.md
index 08fcb5472..d3205caf6 100644
--- a/docs/superpowers/notes/census-proposal-sec-mail-user-boundary.md
+++ b/docs/superpowers/notes/census-proposal-sec-mail-user-boundary.md
@@ -4,65 +4,10 @@ Ruling `census-ruling-sec-mail-user-boundary`. Every change below keeps the disp
| unit | change | read sites | what changed |
|---|---|---|---|
-| `cmd/console/main.go` `serve` | body | unchanged (3) | reads CARLOS_MAIL_USER_BOUNDARY (an environment variable, not a file) and log.Fatals when the mail region is set without a valid boundary ARN; passes it to mail.NewAWSProvisioner through a closure; with CARLOS_MAIL_BOUNDARY_SWEEP=1 starts sweepMailBoundaries (IAM calls only, refused under CARLOS_HOST_CLAIM before any backend call); no file or bucket read is added, the same three /data literals moved |
+| `cmd/console/main.go` `serve` | body | unchanged (3) | reads CARLOS_MAIL_USER_BOUNDARY and passes it to mail.NewAWSProvisioner; runs the one-shot boundary sweep behind CARLOS_MAIL_BOUNDARY_SWEEP=1, refused under CARLOS_HOST_CLAIM |
Envelopes re-pinned: `cmd/console`, `internal/mail`.
-Seal: `09e20ea4bbc534710334e632e903878656dabfb5817d4d96c6e7c56508172148` → `0704f610da881f419f2bc7138a69429c1ceeb1d220b5de24cfda9c0ab91aea02`.
+Seal: `0ca3baafa26f59c0b7d8c6d6a39237d074afb71f885b7fbd50debebab5bf89f2` → `b64b923281b55c7309e5ecb440cbc0869127e43b6f963849eb30257abb9fba6c`.
Overlay: `internal/provision/testdata/plan14-root-reads-sec-mail-user-boundary.tsv`, appended to `internal/provision/testdata/plan14-root-reads-chain.tsv`.
-
-## Branch context
-
-The console's AWS role may call `iam:CreateUser`, `iam:PutUserPolicy` and
-`iam:CreateAccessKey` on `user/carlos-mail/*`, and `internal/mail`'s AWS
-provisioner created those users with no permissions boundary. Anyone able to
-act as the role could create a user there, give it an administrator inline
-policy and mint it a key. Object storage closed the same gap with
-`CARLOS_STORE_USER_BOUNDARY`; this branch mirrors it for mail.
-
-- `mail.NewAWSProvisioner` takes a boundary ARN and refuses an empty or
- malformed one before any AWS call. `EnsureUser` passes it to `CreateUser`
- as `PermissionsBoundary`, and refuses to act without one.
-- When `CreateUser` reports the user exists, `EnsureUser` reads the user's
- boundary from the `GetUser` it already made. If it is missing or
- different, `PutUserPermissionsBoundary` sets the configured one before
- `PutUserPolicy` runs. Nothing calls `DeleteUserPermissionsBoundary`.
-- `cmd/console serve` reads `CARLOS_MAIL_USER_BOUNDARY` and stops with
- `log.Fatal` (through `mailUserBoundary`) when `CARLOS_MAIL_REGION` is set
- and the boundary is empty or not a customer managed policy ARN
- (`arn:<partition>:iam::<12 digits>:policy/<path><name>`, IAM's name
- characters only, no control characters).
-- Astra's review round: `MintKey` also reads the user and backfills the
- boundary before `CreateAccessKey`, because rotation mints without
- `EnsureUser`. And `CARLOS_MAIL_BOUNDARY_SWEEP=1` runs
- `sweepMailBoundaries` once at console start: `iam:ListUsers` under
- `/carlos-mail/`, `GetUser` for each, `PutUserPermissionsBoundary` where it
- is missing or different, and a last log line saying all N users are
- bounded or which are not. IAM only, no file or bucket read. Refused under
- `CARLOS_HOST_CLAIM` before any backend call (Astra round 2).
-
-None of this runs as root. The changed code runs in the console
-(`cmd/console serve` under `console.service`, user `carlos-console`). The
-root `carlos` binary links neither `internal/console` nor `internal/mail`
-(`TestRootBinaryLinksNoConsoleOrMail`). No root read is added.
-
-
-Envelopes re-pinned: `cmd/console` (serve, mailUserBoundary, sweepMailBoundaries, doc comments, tests) and `internal/mail` (aws.go boundary handling, new boundarysweep.go, new aws_iam_test.go).
-
-## Claim authority census (TestClaimAuthoritySourceCensus)
-
-`internal/console/testdata/claim-authority-sites.txt` gains one line, 151 to
-152 calls:
-
- ../../cmd/console/main.go|serve|mail.NewAWSProvisioner(ctx, region, mailBoundary)
-
-There was no old entry to replace. On main, `serve` handed
-`mail.NewAWSProvisioner` to `mailBackends` as a function value, which the
-census does not count as a call. The boundary has to reach the constructor, so
-`serve` now wraps it in a closure, and the closure's call is the site. It is
-the same constructor `mailBackends` already called through its parameter,
-with one more argument, the boundary ARN.
-
-Record-write census (9B8): unchanged (the provision package's census tests
-pass without an overlay). Claim routes: unchanged. go.mod: unchanged.
diff --git a/docs/superpowers/notes/census-proposal-sec-sidecar-create-gate.md b/docs/superpowers/notes/census-proposal-sec-sidecar-create-gate.md
new file mode 100644
index 000000000..7fe04527e
--- /dev/null
+++ b/docs/superpowers/notes/census-proposal-sec-sidecar-create-gate.md
@@ -0,0 +1,13 @@
+# Census approval: sec-sidecar-create-gate
+
+Ruling `census-ruling-sec-sidecar-create-gate`. Every change below keeps the disposition it already has, so it needs an approval, not a decision.
+
+| unit | change | read sites | what changed |
+|---|---|---|---|
+| `cmd/console/main.go` `serve` | body | unchanged (3) | serve now parses CARLOS_SIDECAR_ACCOUNTS (sidecarAccounts, a pure env parse with log.Fatalf on a bad or empty-list value) before opening anything and passes it into console.Config.SidecarAccounts. No read was added: the same 3 root read sites. |
+
+Envelopes re-pinned: `cmd/console`, `internal/console`.
+
+Seal: `945934f5978ace14dc5a9bae0ed69dfabf29b994504fc843c03d4a633ecd6c6a` → `0ca3baafa26f59c0b7d8c6d6a39237d074afb71f885b7fbd50debebab5bf89f2`.
+
+Overlay: `internal/provision/testdata/plan14-root-reads-sec-sidecar-create-gate.tsv`, appended to `internal/provision/testdata/plan14-root-reads-chain.tsv`.
diff --git a/go.mod b/go.mod
index 5a0e3cad5..c320d08df 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/carlosframework/platform
-go 1.26
+go 1.26.0
require (
github.com/aws/aws-sdk-go-v2 v1.44.0
@@ -21,12 +21,12 @@ require (
github.com/aws/smithy-go v1.28.1
github.com/benbjohnson/litestream v0.5.14
github.com/pion/logging v0.2.4
- github.com/pion/stun/v3 v3.0.1
+ github.com/pion/stun/v3 v3.1.5
github.com/pion/turn/v4 v4.1.4
github.com/superfly/ltx v0.5.1
- golang.org/x/crypto v0.53.0
- golang.org/x/net v0.56.0
- golang.org/x/sys v0.46.0
+ golang.org/x/crypto v0.57.0
+ golang.org/x/net v0.60.0
+ golang.org/x/sys v0.48.0
modernc.org/sqlite v1.55.0
)
@@ -60,10 +60,9 @@ require (
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
- github.com/pion/dtls/v3 v3.0.7 // indirect
+ github.com/pion/dtls/v3 v3.1.4 // indirect
github.com/pion/randutil v0.1.0 // indirect
- github.com/pion/transport/v3 v3.0.8 // indirect
- github.com/pion/transport/v4 v4.0.1 // indirect
+ github.com/pion/transport/v4 v4.0.2 // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
@@ -73,8 +72,9 @@ require (
github.com/tetratelabs/wazero v1.2.1 // indirect
github.com/wasilibs/go-re2 v1.3.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
- golang.org/x/sync v0.21.0 // indirect
- golang.org/x/text v0.38.0 // indirect
+ golang.org/x/mod v0.41.0 // indirect
+ golang.org/x/sync v0.23.0 // indirect
+ golang.org/x/text v0.42.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
modernc.org/libc v1.74.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
@@ -86,8 +86,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sns v1.43.0
github.com/klauspost/compress v1.18.0
github.com/oschwald/maxminddb-golang v1.13.1
- golang.org/x/mod v0.37.0
- golang.org/x/tools v0.47.0
+ golang.org/x/tools v0.49.0
)
replace amadan.net/carlos/platform.git/launchctx => ./launchctx
diff --git a/go.sum b/go.sum
index 4558d7955..a7dd6e987 100644
--- a/go.sum
+++ b/go.sum
@@ -154,18 +154,16 @@ github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q=
-github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8=
+github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY=
+github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
-github.com/pion/stun/v3 v3.0.1 h1:jx1uUq6BdPihF0yF33Jj2mh+C9p0atY94IkdnW174kA=
-github.com/pion/stun/v3 v3.0.1/go.mod h1:RHnvlKFg+qHgoKIqtQWMOJF52wsImCAf/Jh5GjX+4Tw=
-github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc=
-github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
-github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
-github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
+github.com/pion/stun/v3 v3.1.5 h1:Y1FHlhaI6+4UoC5i/zQf4F7JvdZtB24/05oyy/GF1x8=
+github.com/pion/stun/v3 v3.1.5/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
+github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
+github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -214,35 +212,25 @@ go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
-golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
-golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
-golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
-golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
-golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
-golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
-golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
-golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
-golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
-golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
-golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
-golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
+golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
+golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
+golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
+golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
+golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
+golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
-golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
-golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
+golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
-golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
-golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
-golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
-golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
-golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
-golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
-golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
-golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
-golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
+golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
+golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
+golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
+golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
+golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA=
google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk=
diff --git a/infra/modules/region-host/main.tf b/infra/modules/region-host/main.tf
index b35ecc196..11fca1987 100644
--- a/infra/modules/region-host/main.tf
+++ b/infra/modules/region-host/main.tf
@@ -490,6 +490,19 @@ resource "aws_instance" "host" {
iam_instance_profile = aws_iam_instance_profile.host.name
user_data = var.user_data
user_data_replace_on_change = false
+ # IMDSv2 only, one hop. With the block absent EC2 leaves IMDSv1 on, and a
+ # plain GET from any request-forgery bug in a tenant app reads this
+ # role's credentials; v2 needs a PUT with a header first, which those
+ # bugs rarely give an attacker. Hop limit 1 keeps the token from leaving
+ # the box. Changing these on a running instance is in place, no restart.
+ # The flagship boxes were switched by hand on 2026-10-23; this keeps a
+ # replaced box from coming back on v1. The scanner host has done the same
+ # since it was written.
+ metadata_options {
+ http_endpoint = "enabled"
+ http_tokens = "required"
+ http_put_response_hop_limit = 1
+ }
root_block_device {
volume_size = var.root_volume_size
volume_type = "gp3"
diff --git a/infra/modules/region-host/tests/instance_metadata.tftest.hcl b/infra/modules/region-host/tests/instance_metadata.tftest.hcl
new file mode 100644
index 000000000..a2c60aa2e
--- /dev/null
+++ b/infra/modules/region-host/tests/instance_metadata.tftest.hcl
@@ -0,0 +1,49 @@
+# Plan-time tests for the host instance's metadata service settings.
+# mock_provider means no AWS account and no credentials.
+mock_provider "aws" {}
+
+override_data {
+ target = data.aws_vpcs.default
+ values = { ids = ["vpc-0aaaaaaaaaaaaaaaa"] }
+}
+
+override_data {
+ target = data.aws_caller_identity.current
+ values = { account_id = "123456789012" }
+}
+
+# The termination queue's ARN feeds an EventBridge target, which the provider
+# validates as an ARN; the mock's random string is not one.
+override_resource {
+ target = aws_sqs_queue.terminations
+ values = { arn = "arn:aws:sqs:eu-west-1:123456789012:carlos-test-host-terminations" }
+}
+
+variables {
+ name = "carlos-test-ie"
+ sg_description = "carlos test host"
+ iam_name = "carlos-test-host"
+ instance_type = "t4g.small"
+ root_volume_size = 20
+ user_data = "#!/bin/sh\n"
+ deployment_bucket_arn = "arn:aws:s3:::carlos-test-platform"
+ data_bucket_arn = "arn:aws:s3:::carlos-test-ie"
+ ssm_parameter_arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/carlos/*"
+}
+
+
+run "imdsv2_only" {
+ command = plan
+ assert {
+ condition = aws_instance.host.metadata_options[0].http_tokens == "required"
+ error_message = "the host requires IMDSv2 tokens"
+ }
+ assert {
+ condition = aws_instance.host.metadata_options[0].http_put_response_hop_limit == 1
+ error_message = "the IMDS token must not leave the box (hop limit 1)"
+ }
+ assert {
+ condition = aws_instance.host.metadata_options[0].http_endpoint == "enabled"
+ error_message = "the endpoint stays on: the box role is how the host reaches AWS"
+ }
+}
diff --git a/internal/console/apisidecars.go b/internal/console/apisidecars.go
index 698b7daa4..17c8aa12d 100644
--- a/internal/console/apisidecars.go
+++ b/internal/console/apisidecars.go
@@ -198,7 +198,8 @@ func (s *Server) handleAPISidecarGet(w http.ResponseWriter, r *http.Request) {
// handleAPISidecarCreate is the point of the feature: one create-only write
// to the bucket, in the family's refusal order — gate, budget, readiness
-// (instanceGate), manifest, name, region, channel, shape, write.
+// (instanceGate), manifest, name, region, channel, shape, pause
+// (sidecarsgate.go), write.
//
// Every field policy governs is stamped from the MANIFEST, never from the
// request. The body has no pool field to lie in, no host field (the host is
@@ -311,6 +312,12 @@ func (s *Server) handleAPISidecarCreate(w http.ResponseWriter, r *http.Request)
jsonError(w, http.StatusBadRequest, err.Error())
return
}
+ // The sidecar pause (sidecarsgate.go): after every check above, so a
+ // stranger still meets the 404 and a bad request its 400, and before
+ // the cap's listing, the leftover rail and either write arm.
+ if s.refuseSidecarsPaused(w, acct, app, "create") {
+ return
+ }
// Plans 3a: the plan caps workers per app, under the account lock.
defer s.planLock(acct)()
if err := s.checkSidecars(r.Context(), acct, app); err != nil {
@@ -586,6 +593,11 @@ func (s *Server) handleAPISidecarEnable(w http.ResponseWriter, r *http.Request)
return
}
}
+ // The sidecar pause (sidecarsgate.go): after authentication, membership
+ // and validation, before the manifest read and its write.
+ if s.refuseSidecarsPaused(w, acct, app, "enable") {
+ return
+ }
ctx, cancel := context.WithTimeout(r.Context(), instanceStoreTimeout)
defer cancel()
// Read WITH the etag and write guarded by it below: a read-modify-write
diff --git a/internal/console/server.go b/internal/console/server.go
index e7c940087..0c186db2f 100644
--- a/internal/console/server.go
+++ b/internal/console/server.go
@@ -855,6 +855,13 @@ type Config struct {
// wherever the two differ by anything but that label; the relay's DNS
// names are operator-managed and this console cannot discover them.
PlatformDomain string
+ // SidecarAccounts is the operator allowlist of account sqids that may
+ // still enable and create native sidecars (CARLOS_SIDECAR_ACCOUNTS,
+ // normalized by cmd/console: trimmed, lower-cased, each a valid sqid).
+ // Empty is no gate at all, every deployment's shape before the list
+ // existed. Set, every other account meets sidecarsPaused on the two
+ // doors that bring a sidecar into being (sidecarsgate.go).
+ SidecarAccounts []string
}
// Server is the console's http.Handler.
diff --git a/internal/console/sidecarsgate.go b/internal/console/sidecarsgate.go
new file mode 100644
index 000000000..002d4c8e5
--- /dev/null
+++ b/internal/console/sidecarsgate.go
@@ -0,0 +1,64 @@
+// The sidecar pause: an operator allowlist in front of the two doors that
+// bring a native sidecar into being.
+//
+// Why it exists: a native sidecar runs on the flat tier
+// (activator.ExecBackend.StartWorkerStaged) as the box's carlos uid with no
+// mount namespace, so on its box it can read and write every nested
+// tenant's /data/instances/<iid>. The fix is nested sidecars (amadan
+// discussion #370). Until that lands, an operator lists the accounts that
+// may still enable or create one (CARLOS_SIDECAR_ACCOUNTS) and every other
+// account is refused.
+//
+// The doors, and why there are only two. A sidecar runs only from a record
+// at apps/<sqid>/<app>/sidecars/<name>.json, which the box's sidecar
+// reconcile pass (or, under host claim, the root claim path) turns into a
+// Kind "sidecar" route, and it takes its argv and ceilings from
+// apps/<sqid>/<app>/sidecars.json. The console writes the record in exactly
+// one place (handleAPISidecarCreate, both the CreateSidecarRecord and the
+// host-claim beginClaimCreate arms) and the manifest in exactly one place
+// (handleAPISidecarEnable). Both call refuseSidecarsPaused. Ship and promote
+// publish releases and channel pointers, never a sidecar manifest; restore
+// clears an app's trash marker and brings back only sidecars that already
+// existed; carlos run needs a nested instance. Store mode (`carlos sidecars
+// enable` against the bucket) is the operator's own bucket credential and
+// is not a console door.
+//
+// What it leaves alone, on purpose: list, get, wake and delete. Existing
+// sidecars keep running, and an account taken off the list must still be
+// able to retire its own.
+package console
+
+import (
+ "log"
+ "net/http"
+ "slices"
+)
+
+// sidecarsPausedMsg is the one sentence a refused account reads.
+// copy: approved by Paul 2026-10-24 (copy review).
+const sidecarsPausedMsg = "sidecars are paused for this account. existing sidecars keep running, and you can still delete them"
+
+// sidecarsPausedCode is the refusal's machine-readable code, beside the
+// sentence the way plan caps carry "cap".
+const sidecarsPausedCode = "sidecars-paused"
+
+// sidecarsPaused reports whether acct is refused new sidecars: the
+// allowlist is set and does not name the account's sqid.
+func (s *Server) sidecarsPaused(acct *Account) bool {
+ return len(s.cfg.SidecarAccounts) > 0 && !slices.Contains(s.cfg.SidecarAccounts, acct.Sqid)
+}
+
+// refuseSidecarsPaused answers 403 sidecars-paused and reports true when
+// acct may not enable or create a sidecar. Call it AFTER the door's
+// authentication, membership and validation checks, so a non-member still
+// gets the byte-identical 404 and the gate never confirms that an account
+// exists, and BEFORE any bucket write. Request-path refusals are not
+// audited in this console (the plan cap's is not either), so it logs.
+func (s *Server) refuseSidecarsPaused(w http.ResponseWriter, acct *Account, app, verb string) bool {
+ if !s.sidecarsPaused(acct) {
+ return false
+ }
+ log.Printf("console: api sidecars %s %s/%s: refused, account not in CARLOS_SIDECAR_ACCOUNTS", verb, acct.Sqid, app)
+ writeJSON(w, http.StatusForbidden, map[string]any{"error": sidecarsPausedMsg, "code": sidecarsPausedCode})
+ return true
+}
diff --git a/internal/console/sidecarsgate_test.go b/internal/console/sidecarsgate_test.go
new file mode 100644
index 000000000..7f0f90dd0
--- /dev/null
+++ b/internal/console/sidecarsgate_test.go
@@ -0,0 +1,193 @@
+package console
+
+// The sidecar pause (sidecarsgate.go), through the real Server, its router
+// and the cliRoute auth stack the rest of apisidecars_test.go uses. The
+// properties, in the order they would hurt if lost: with the allowlist set,
+// an account it does not name cannot enable or create a sidecar by any
+// credential that reaches those doors, and nothing is written to the bucket
+// when it is refused; a stranger still gets the byte-identical 404, so the
+// gate never confirms an account exists; existing sidecars stay listable,
+// wakeable and deletable; a named account is unaffected; and with the
+// allowlist unset nothing changes at all.
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "slices"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/svcrole"
+)
+
+// sidecarAppKeys is every bucket key under the fixture app's prefix, sorted: the
+// refused calls must leave it exactly as it was.
+func sidecarAppKeys(t *testing.T, fx *consoleFixture) []string {
+ t.Helper()
+ keys, err := fx.rel.List(context.Background(), release.AppPrefix("bab", "titogo"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ slices.Sort(keys)
+ return keys
+}
+
+func assertSidecarsPaused(t *testing.T, what string, status int, body string) {
+ t.Helper()
+ if status != http.StatusForbidden {
+ t.Fatalf("%s = %d %s, want 403", what, status, body)
+ }
+ var got struct{ Error, Code string }
+ if err := json.Unmarshal([]byte(body), &got); err != nil {
+ t.Fatalf("%s body %q: %v", what, body, err)
+ }
+ if got.Code != "sidecars-paused" || got.Error != sidecarsPausedMsg {
+ t.Fatalf("%s body = %+v, want code sidecars-paused and the paused sentence", what, got)
+ }
+}
+
+// TestSidecarsPausedRefusesEnableAndCreate: the fixture's account (bab) is
+// not on the list, so enable and create are refused for the member, the
+// legacy app token and an instance-role service principal, and the bucket
+// is untouched.
+func TestSidecarsPausedRefusesEnableAndCreate(t *testing.T) {
+ fx, member := instanceFixture(t, "code-side-paused")
+ fx.srv.cfg.SidecarAccounts = []string{"cdef"}
+ u := sidecarsURL(fx, "bab", "titogo")
+
+ // Enable with no manifest yet: refused, and no manifest appears.
+ before := sidecarAppKeys(t, fx)
+ status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), member, `{"channel":"beta"}`)
+ assertSidecarsPaused(t, "paused enable, no manifest", status, out)
+ if after := sidecarAppKeys(t, fx); !slices.Equal(before, after) {
+ t.Fatalf("a refused enable changed the bucket: %v -> %v", before, after)
+ }
+
+ // An operator-written manifest (store mode, not a console door) is
+ // there; enable still may not rewrite it.
+ enableSidecars(t, fx, "titogo", titoSidecarManifest())
+ manifest, _, err := fx.rel.Get(context.Background(), provision.SidecarManifestKey("bab", "titogo"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ status, _, out = rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), member, `{"channel":"beta","args":["evil"]}`)
+ assertSidecarsPaused(t, "paused enable over a manifest", status, out)
+ if now, _, err := fx.rel.Get(context.Background(), provision.SidecarManifestKey("bab", "titogo")); err != nil || string(now) != string(manifest) {
+ t.Fatalf("a refused enable rewrote the manifest: %s -> %s (%v)", manifest, now, err)
+ }
+
+ // Create, by every credential the create door admits.
+ appTok, err := fx.st.NewAppToken(OpsAccountID, "titogo", "alice@keymail.dev", "signup", time.Now())
+ if err != nil {
+ t.Fatal(err)
+ }
+ svc := serviceToken(t, fx.st, OpsAccountID, "router", "titogo", svcrole.Instance)
+ before = sidecarAppKeys(t, fx)
+ for _, c := range []struct{ what, token string }{
+ {"member create", member},
+ {"app token create", appTok},
+ {"instance service principal create", svc},
+ } {
+ status, _, out := rawReq(t, http.MethodPost, u, c.token, `{"name":"bot","region":"ie"}`)
+ assertSidecarsPaused(t, c.what, status, out)
+ }
+ if after := sidecarAppKeys(t, fx); !slices.Equal(before, after) {
+ t.Fatalf("a refused create changed the bucket: %v -> %v", before, after)
+ }
+ if keys, err := fx.rel.List(context.Background(), provision.SidecarsPrefix("bab", "titogo")); err != nil || len(keys) != 0 {
+ t.Fatalf("refused creates left records %v (%v)", keys, err)
+ }
+
+ // The gate comes after membership: a stranger and an unknown account
+ // still get the byte-identical 404, never the 403 that would confirm
+ // the account exists and is paused.
+ want := canonicalNotFound(t)
+ eve := cliToken(t, fx, "eve@keymail.dev", "code-side-paused-eve")
+ for _, c := range []struct{ what, method, url, token, body string }{
+ {"non-member enable", http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), eve, `{}`},
+ {"non-member create", http.MethodPost, u, eve, `{"name":"bot","region":"ie"}`},
+ {"unknown account create", http.MethodPost, sidecarsURL(fx, "zzzz", "titogo"), member, `{"name":"bot","region":"ie"}`},
+ {"unclaimed app create", http.MethodPost, sidecarsURL(fx, "bab", "ghost-app"), member, `{"name":"bot","region":"ie"}`},
+ } {
+ status, h, out := rawReq(t, c.method, c.url, c.token, c.body)
+ assertNotFound(t, c.what, want, status, h, out)
+ }
+ // And validation still answers first for a member.
+ if status, _, out := rawReq(t, http.MethodPost, u, member, `{"name":"-nope-","region":"ie"}`); status != http.StatusBadRequest {
+ t.Fatalf("paused create with a bad name = %d %s, want the 400 first", status, out)
+ }
+}
+
+// TestSidecarsPausedLeavesExistingSidecarsAlone: a sidecar created before the
+// pause is still listed, read, woken and deleted by the paused account, by
+// the member and by the instance service principal.
+func TestSidecarsPausedLeavesExistingSidecarsAlone(t *testing.T) {
+ fx, member := instanceFixture(t, "code-side-paused-existing")
+ enableSidecars(t, fx, "titogo", titoSidecarManifest())
+ u := sidecarsURL(fx, "bab", "titogo")
+ for _, name := range []string{"bot", "bot2"} {
+ if status, _, out := rawReq(t, http.MethodPost, u, member, `{"name":"`+name+`","region":"ie"}`); status != http.StatusCreated {
+ t.Fatalf("create %s before the pause = %d %s", name, status, out)
+ }
+ }
+ fx.srv.cfg.SidecarAccounts = []string{"cdef"}
+ svc := serviceToken(t, fx.st, OpsAccountID, "router", "titogo", svcrole.Instance)
+
+ for _, c := range []struct{ what, method, url string }{
+ {"list", http.MethodGet, u},
+ {"get", http.MethodGet, u + "/bot"},
+ {"wake", http.MethodPost, u + "/bot/wake"},
+ } {
+ if status, _, out := rawReq(t, c.method, c.url, member, ""); status != http.StatusOK {
+ t.Fatalf("paused %s = %d %s, want 200", c.what, status, out)
+ }
+ }
+ for _, c := range []struct{ name, token string }{{"bot", member}, {"bot2", svc}} {
+ if status, _, out := rawReq(t, http.MethodDelete, u+"/"+c.name, c.token, ""); status != http.StatusOK {
+ t.Fatalf("paused delete of %s = %d %s, want 200", c.name, status, out)
+ }
+ if _, _, err := fx.rel.Get(context.Background(), provision.SidecarRecordKey("bab", "titogo", c.name)); err == nil {
+ t.Fatalf("the record for %s survived its delete", c.name)
+ }
+ }
+}
+
+// TestSidecarsPauseAllowlistAndUnset: an account the list names, and a
+// console with no list at all, enable and create exactly as before.
+func TestSidecarsPauseAllowlistAndUnset(t *testing.T) {
+ for _, c := range []struct {
+ what string
+ accts []string
+ }{
+ {"allowlisted", []string{"cdef", "bab"}},
+ {"unset", nil},
+ } {
+ t.Run(c.what, func(t *testing.T) {
+ fx, member := instanceFixture(t, "code-side-allow-"+c.what)
+ fx.srv.cfg.SidecarAccounts = c.accts
+ status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), member, `{"channel":"stable"}`)
+ if status != http.StatusOK {
+ t.Fatalf("enable = %d %s, want 200", status, out)
+ }
+ // The pool and regions are operator policy the enable door never
+ // sets; put them as the operator would so create can place.
+ enableSidecars(t, fx, "titogo", titoSidecarManifest())
+ u := sidecarsURL(fx, "bab", "titogo")
+ if status, _, out := rawReq(t, http.MethodPost, u, member, `{"name":"bot","region":"ie"}`); status != http.StatusCreated {
+ t.Fatalf("member create = %d %s, want 201", status, out)
+ }
+ svc := serviceToken(t, fx.st, OpsAccountID, "router", "titogo", svcrole.Instance)
+ if status, _, out := rawReq(t, http.MethodPost, u, svc, `{"name":"bot2","region":"ie"}`); status != http.StatusCreated {
+ t.Fatalf("service principal create = %d %s, want 201", status, out)
+ }
+ for _, name := range []string{"bot", "bot2"} {
+ if _, err := provision.GetSidecarRecord(context.Background(), fx.rel, "bab", "titogo", name); err != nil {
+ t.Fatalf("record %s: %v", name, err)
+ }
+ }
+ })
+ }
+}
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go
index 525edfb2c..ec0176aee 100644
--- a/internal/provision/plan9_b8_census_test.go
+++ b/internal/provision/plan9_b8_census_test.go
@@ -366,7 +366,7 @@ func TestPlan9B8RecordWriteCensus(t *testing.T) {
if len(got) < 100 {
t.Fatalf("census found only %d units under %s; the walk is broken", len(got), root)
}
- inventory := dbRegionForward3WriteCensusInventory()
+ inventory := secSidecarCreateGateWriteCensusInventory()
problems := append(censusDiff(inventory, got), censusPolicy(inventory)...)
if len(problems) > 0 {
t.Fatalf("record-write census rejected %d site(s); update the explicit inventory only after reviewing the source:\n %s", len(problems), strings.Join(problems, "\n "))
diff --git a/internal/provision/sec_sidecar_create_gate_census_test.go b/internal/provision/sec_sidecar_create_gate_census_test.go
new file mode 100644
index 000000000..c4ecffc32
--- /dev/null
+++ b/internal/provision/sec_sidecar_create_gate_census_test.go
@@ -0,0 +1,27 @@
+package provision
+
+// Record-write census entries for the sidecar pause (sec-sidecar-create-gate),
+// layered on dbRegionForward3WriteCensusInventory (main's last at 65ca4aca).
+// handleAPISidecarCreate gains one early return, refuseSidecarsPaused
+// (internal/console/sidecarsgate.go), after the shape check and before the
+// plan cap, the leftover rail and both write arms. Its write is unchanged:
+// the same CreateSidecarRecord create-only write, or the host-claim create.
+// No write site is added; the refusal writes nothing.
+func secSidecarCreateGateWriteCensusInventory() []censusEntry {
+ entries := append([]censusEntry(nil), dbRegionForward3WriteCensusInventory()...)
+ replacements := []censusEntry{
+ {"internal/console/apisidecars.go", "Server.handleAPISidecarCreate", "record-writer", "key:CreateSidecarRecord", "72f4d44028fbc46ed2ed1adca6154f385b62e143f47e597a49b6e28570b2ddab"},
+ }
+ for _, r := range replacements {
+ found := false
+ for i, e := range entries {
+ if e.File == r.File && e.Unit == r.Unit {
+ entries[i], found = r, true
+ }
+ }
+ if !found {
+ entries = append(entries, r)
+ }
+ }
+ return entries
+}
diff --git a/internal/provision/testdata/plan14-root-reads-chain.tsv b/internal/provision/testdata/plan14-root-reads-chain.tsv
index 9b02c3c9d..200795c50 100644
--- a/internal/provision/testdata/plan14-root-reads-chain.tsv
+++ b/internal/provision/testdata/plan14-root-reads-chain.tsv
@@ -1,4 +1,7 @@
# Census chain manifest: overlays loaded after rootReadChain's own, in order.
# One per line: <overlay file name under testdata>\t<the seal it leaves>.
# make census-propose appends here; nothing else edits this file.
-plan14-root-reads-sec-mail-user-boundary.tsv 0704f610da881f419f2bc7138a69429c1ceeb1d220b5de24cfda9c0ab91aea02
+plan14-root-reads-sec-host-imdsv2-module.tsv 945934f5978ace14dc5a9bae0ed69dfabf29b994504fc843c03d4a633ecd6c6a
+plan14-root-reads-sec-deps-dos-bumps.tsv 945934f5978ace14dc5a9bae0ed69dfabf29b994504fc843c03d4a633ecd6c6a
+plan14-root-reads-sec-sidecar-create-gate.tsv 0ca3baafa26f59c0b7d8c6d6a39237d074afb71f885b7fbd50debebab5bf89f2
+plan14-root-reads-sec-mail-user-boundary.tsv b64b923281b55c7309e5ecb440cbc0869127e43b6f963849eb30257abb9fba6c
diff --git a/internal/provision/testdata/plan14-root-reads-sec-deps-dos-bumps.tsv b/internal/provision/testdata/plan14-root-reads-sec-deps-dos-bumps.tsv
new file mode 100644
index 000000000..02c33bd1e
--- /dev/null
+++ b/internal/provision/testdata/plan14-root-reads-sec-deps-dos-bumps.tsv
@@ -0,0 +1,3 @@
+# Census overlay sec-deps-dos-bumps, written by census-propose (#369). census-ruling-sec-deps-dos-bumps, approved by the census owner before the merge; the approval card is docs/superpowers/notes/census-proposal-sec-deps-dos-bumps.md.
+# 0 body, 0 sites, 1 envelope: each changed unit re-frozen at its existing disposition with its claim appended.
+@coverage . ff7e60e8734238290264ba3b27931d9477811d9a37921a205b1b7244a610bef0
diff --git a/internal/provision/testdata/plan14-root-reads-sec-host-imdsv2-module.tsv b/internal/provision/testdata/plan14-root-reads-sec-host-imdsv2-module.tsv
new file mode 100644
index 000000000..c4ac824f6
--- /dev/null
+++ b/internal/provision/testdata/plan14-root-reads-sec-host-imdsv2-module.tsv
@@ -0,0 +1,5 @@
+# Census overlay sec-host-imdsv2-module, written by census-propose (#369). census-ruling-sec-host-imdsv2-module, approved by the census owner before the merge; the approval card is docs/superpowers/notes/census-proposal-sec-host-imdsv2-module.md.
+# 1 body, 0 sites, 1 envelope: each changed unit re-frozen at its existing disposition with its claim appended.
+@legacy bf41de79166910448a12a9ba5e5568bc7365193b909d5ebbe942facce8ecb95c infra/modules/region-host/main.tf external legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=tofu on an operator's machine with the operator's AWS credentials (no box unit; no binary embeds the file): main's module with one optional aws_iam_role_policy, host_console_mail, count 0 unless console_mail_from is set; it grants the host role ses:SendEmail on the one sending identity, the carlos-bab configuration set and its tenants, conditioned on ses:FromAddress equal to console_mail_from; no read is added and nothing on a box reads the file;rev3=census-ruling-sec-host-imdsv2-module;basis=aws_instance.host gains metadata_options (IMDSv2 required, hop limit 1, endpoint enabled), matching scanner-host; tofu configuration only, run on an operator's machine; no read, file or record write is added; applies in place on a running instance with no restart file:external@0 8ec89e75ca0a0d2ddde74bffe226adb5a5935249fd41590bcce1623c5201ab47
+infra/modules/region-host/main.tf external legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=tofu on an operator's machine with the operator's AWS credentials (no box unit; no binary embeds the file): main's module with one optional aws_iam_role_policy, host_console_mail, count 0 unless console_mail_from is set; it grants the host role ses:SendEmail on the one sending identity, the carlos-bab configuration set and its tenants, conditioned on ses:FromAddress equal to console_mail_from; no read is added and nothing on a box reads the file;rev3=census-ruling-sec-host-imdsv2-module;basis=aws_instance.host gains metadata_options (IMDSv2 required, hop limit 1, endpoint enabled), matching scanner-host; tofu configuration only, run on an operator's machine; no read, file or record write is added; applies in place on a running instance with no restart file:external@0 8ec89e75ca0a0d2ddde74bffe226adb5a5935249fd41590bcce1623c5201ab47
+@coverage infra/modules/region-host 5f1b1bbc02603a54e136ff004e2495bef2b0f22054e058b5930a539749b1b558
diff --git a/internal/provision/testdata/plan14-root-reads-sec-mail-user-boundary.tsv b/internal/provision/testdata/plan14-root-reads-sec-mail-user-boundary.tsv
index e9de3c791..10cbe805c 100644
--- a/internal/provision/testdata/plan14-root-reads-sec-mail-user-boundary.tsv
+++ b/internal/provision/testdata/plan14-root-reads-sec-mail-user-boundary.tsv
@@ -1,6 +1,6 @@
# Census overlay sec-mail-user-boundary, written by census-propose (#369). census-ruling-sec-mail-user-boundary, approved by the census owner before the merge; the approval card is docs/superpowers/notes/census-proposal-sec-mail-user-boundary.md.
# 1 body, 0 sites, 2 envelope: each changed unit re-frozen at its existing disposition with its claim appended.
-@legacy 02f3fd186a4a42fee179d49ea0321ea83c59bbe23e340b9b6b586e1ddab973cd cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=PROPOSED-scan-demand-revival;basis=Console service wires the optional callback signer and starts the claim-guarded queue worker. The same three data path literals move; it does not add a privileged reader.;rev3=PROPOSED-email-signin;identity=the console (cmd/console serve under console.service, user carlos-console);basis=passes CARLOS_CONSOLE_NAME to NewServer as Config.ConsoleName, the name the console's emails use; the same three /data literals at the same offsets; no read added;rev3=PROPOSED-s3-change-index;identity=the console (cmd/console serve under console.service, user carlos-console);basis=arms the record view when CARLOS_CHANGE_INDEX=read (armChangeIndex) for the run and seed sweeps and starts its backstop loop; the same three /data literals at the same offsets; no read added;rev3=census-ruling-db-region-forward-2;identity=the console (cmd/console serve under console.service, user carlos-console);basis=parses CARLOS_DB_FORWARD (dbForward) into Config.DBForward and builds its http.Server with console.NewHTTPServer, the same two timeouts; the same three /data literals at the same offsets; no read added;rev3=census-ruling-sec-mail-user-boundary;basis=reads CARLOS_MAIL_USER_BOUNDARY (an environment variable, not a file) and log.Fatals when the mail region is set without a valid boundary ARN; passes it to mail.NewAWSProvisioner through a closure; with CARLOS_MAIL_BOUNDARY_SWEEP=1 starts sweepMailBoundaries (IAM calls only, refused under CARLOS_HOST_CLAIM before any backend call); no file or bucket read is added, the same three /data literals moved data-literal@4359,data-literal@5460,data-literal@5697 d0d626616de3c53e94b5853bb6154e0c3efb733fbce99165bfdbd1527bdb9df7
-cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=PROPOSED-scan-demand-revival;basis=Console service wires the optional callback signer and starts the claim-guarded queue worker. The same three data path literals move; it does not add a privileged reader.;rev3=PROPOSED-email-signin;identity=the console (cmd/console serve under console.service, user carlos-console);basis=passes CARLOS_CONSOLE_NAME to NewServer as Config.ConsoleName, the name the console's emails use; the same three /data literals at the same offsets; no read added;rev3=PROPOSED-s3-change-index;identity=the console (cmd/console serve under console.service, user carlos-console);basis=arms the record view when CARLOS_CHANGE_INDEX=read (armChangeIndex) for the run and seed sweeps and starts its backstop loop; the same three /data literals at the same offsets; no read added;rev3=census-ruling-db-region-forward-2;identity=the console (cmd/console serve under console.service, user carlos-console);basis=parses CARLOS_DB_FORWARD (dbForward) into Config.DBForward and builds its http.Server with console.NewHTTPServer, the same two timeouts; the same three /data literals at the same offsets; no read added;rev3=census-ruling-sec-mail-user-boundary;basis=reads CARLOS_MAIL_USER_BOUNDARY (an environment variable, not a file) and log.Fatals when the mail region is set without a valid boundary ARN; passes it to mail.NewAWSProvisioner through a closure; with CARLOS_MAIL_BOUNDARY_SWEEP=1 starts sweepMailBoundaries (IAM calls only, refused under CARLOS_HOST_CLAIM before any backend call); no file or bucket read is added, the same three /data literals moved data-literal@4359,data-literal@5460,data-literal@5697 d0d626616de3c53e94b5853bb6154e0c3efb733fbce99165bfdbd1527bdb9df7
-@coverage cmd/console 655e1cf47dea7740e4d3598075627b6bf015a7434f14389bc8ba8d27cc1ae9f1
+@legacy 9ad86a176bcc0bd04d4d50ac38ad2d0922023da5ecf35b7fe635dd8049daf21e cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=PROPOSED-scan-demand-revival;basis=Console service wires the optional callback signer and starts the claim-guarded queue worker. The same three data path literals move; it does not add a privileged reader.;rev3=PROPOSED-email-signin;identity=the console (cmd/console serve under console.service, user carlos-console);basis=passes CARLOS_CONSOLE_NAME to NewServer as Config.ConsoleName, the name the console's emails use; the same three /data literals at the same offsets; no read added;rev3=PROPOSED-s3-change-index;identity=the console (cmd/console serve under console.service, user carlos-console);basis=arms the record view when CARLOS_CHANGE_INDEX=read (armChangeIndex) for the run and seed sweeps and starts its backstop loop; the same three /data literals at the same offsets; no read added;rev3=census-ruling-db-region-forward-2;identity=the console (cmd/console serve under console.service, user carlos-console);basis=parses CARLOS_DB_FORWARD (dbForward) into Config.DBForward and builds its http.Server with console.NewHTTPServer, the same two timeouts; the same three /data literals at the same offsets; no read added;rev3=census-ruling-sec-sidecar-create-gate;basis=serve now parses CARLOS_SIDECAR_ACCOUNTS (sidecarAccounts, a pure env parse with log.Fatalf on a bad or empty-list value) before opening anything and passes it into console.Config.SidecarAccounts. No read was added: the same 3 root read sites.;rev3=census-ruling-sec-mail-user-boundary;basis=reads CARLOS_MAIL_USER_BOUNDARY and passes it to mail.NewAWSProvisioner; runs the one-shot boundary sweep behind CARLOS_MAIL_BOUNDARY_SWEEP=1, refused under CARLOS_HOST_CLAIM data-literal@4601,data-literal@5702,data-literal@5939 d42709a70b4089043a22fc32db306af83d595eb734f1ce050802ba8e85d413ff
+cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=PROPOSED-scan-demand-revival;basis=Console service wires the optional callback signer and starts the claim-guarded queue worker. The same three data path literals move; it does not add a privileged reader.;rev3=PROPOSED-email-signin;identity=the console (cmd/console serve under console.service, user carlos-console);basis=passes CARLOS_CONSOLE_NAME to NewServer as Config.ConsoleName, the name the console's emails use; the same three /data literals at the same offsets; no read added;rev3=PROPOSED-s3-change-index;identity=the console (cmd/console serve under console.service, user carlos-console);basis=arms the record view when CARLOS_CHANGE_INDEX=read (armChangeIndex) for the run and seed sweeps and starts its backstop loop; the same three /data literals at the same offsets; no read added;rev3=census-ruling-db-region-forward-2;identity=the console (cmd/console serve under console.service, user carlos-console);basis=parses CARLOS_DB_FORWARD (dbForward) into Config.DBForward and builds its http.Server with console.NewHTTPServer, the same two timeouts; the same three /data literals at the same offsets; no read added;rev3=census-ruling-sec-sidecar-create-gate;basis=serve now parses CARLOS_SIDECAR_ACCOUNTS (sidecarAccounts, a pure env parse with log.Fatalf on a bad or empty-list value) before opening anything and passes it into console.Config.SidecarAccounts. No read was added: the same 3 root read sites.;rev3=census-ruling-sec-mail-user-boundary;basis=reads CARLOS_MAIL_USER_BOUNDARY and passes it to mail.NewAWSProvisioner; runs the one-shot boundary sweep behind CARLOS_MAIL_BOUNDARY_SWEEP=1, refused under CARLOS_HOST_CLAIM data-literal@4601,data-literal@5702,data-literal@5939 d42709a70b4089043a22fc32db306af83d595eb734f1ce050802ba8e85d413ff
+@coverage cmd/console 72c886448386811edaf505aebf56e3455b01b8cf3e88909592c56dc7267b84a5
@coverage internal/mail 82a7f2590ced5c1ad6a72b0e62305b4cc157eeade4e8a3159366a41bc3eaca88
diff --git a/internal/provision/testdata/plan14-root-reads-sec-sidecar-create-gate.tsv b/internal/provision/testdata/plan14-root-reads-sec-sidecar-create-gate.tsv
new file mode 100644
index 000000000..ff9e1e70c
--- /dev/null
+++ b/internal/provision/testdata/plan14-root-reads-sec-sidecar-create-gate.tsv
@@ -0,0 +1,6 @@
+# Census overlay sec-sidecar-create-gate, written by census-propose (#369). census-ruling-sec-sidecar-create-gate, approved by the census owner before the merge; the approval card is docs/superpowers/notes/census-proposal-sec-sidecar-create-gate.md.
+# 1 body, 0 sites, 2 envelope: each changed unit re-frozen at its existing disposition with its claim appended.
+@legacy dd17558eb17031da9bd9c517c68fd97ac5456969222be618e4b24c74128813fd cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=PROPOSED-scan-demand-revival;basis=Console service wires the optional callback signer and starts the claim-guarded queue worker. The same three data path literals move; it does not add a privileged reader.;rev3=PROPOSED-email-signin;identity=the console (cmd/console serve under console.service, user carlos-console);basis=passes CARLOS_CONSOLE_NAME to NewServer as Config.ConsoleName, the name the console's emails use; the same three /data literals at the same offsets; no read added;rev3=PROPOSED-s3-change-index;identity=the console (cmd/console serve under console.service, user carlos-console);basis=arms the record view when CARLOS_CHANGE_INDEX=read (armChangeIndex) for the run and seed sweeps and starts its backstop loop; the same three /data literals at the same offsets; no read added;rev3=census-ruling-db-region-forward-2;identity=the console (cmd/console serve under console.service, user carlos-console);basis=parses CARLOS_DB_FORWARD (dbForward) into Config.DBForward and builds its http.Server with console.NewHTTPServer, the same two timeouts; the same three /data literals at the same offsets; no read added;rev3=census-ruling-sec-sidecar-create-gate;basis=serve now parses CARLOS_SIDECAR_ACCOUNTS (sidecarAccounts, a pure env parse with log.Fatalf on a bad or empty-list value) before opening anything and passes it into console.Config.SidecarAccounts. No read was added: the same 3 root read sites. data-literal@4319,data-literal@5420,data-literal@5657 1f1aa5c40845b20922a7740ec15254a47ff5856e1ef14252a189e1af56ddd406
+cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=PROPOSED-scan-demand-revival;basis=Console service wires the optional callback signer and starts the claim-guarded queue worker. The same three data path literals move; it does not add a privileged reader.;rev3=PROPOSED-email-signin;identity=the console (cmd/console serve under console.service, user carlos-console);basis=passes CARLOS_CONSOLE_NAME to NewServer as Config.ConsoleName, the name the console's emails use; the same three /data literals at the same offsets; no read added;rev3=PROPOSED-s3-change-index;identity=the console (cmd/console serve under console.service, user carlos-console);basis=arms the record view when CARLOS_CHANGE_INDEX=read (armChangeIndex) for the run and seed sweeps and starts its backstop loop; the same three /data literals at the same offsets; no read added;rev3=census-ruling-db-region-forward-2;identity=the console (cmd/console serve under console.service, user carlos-console);basis=parses CARLOS_DB_FORWARD (dbForward) into Config.DBForward and builds its http.Server with console.NewHTTPServer, the same two timeouts; the same three /data literals at the same offsets; no read added;rev3=census-ruling-sec-sidecar-create-gate;basis=serve now parses CARLOS_SIDECAR_ACCOUNTS (sidecarAccounts, a pure env parse with log.Fatalf on a bad or empty-list value) before opening anything and passes it into console.Config.SidecarAccounts. No read was added: the same 3 root read sites. data-literal@4319,data-literal@5420,data-literal@5657 1f1aa5c40845b20922a7740ec15254a47ff5856e1ef14252a189e1af56ddd406
+@coverage cmd/console 4865ecf0f8de501d62bcfbd10cc9a4244c3e5c5ee53c6c2ee476b5dba7fd36b2
+@coverage internal/console 3ab92802ced1e88b3b841f122fa64b201d576e9dfd5db7662da0028caf7c78ec