Skip to content

Add aws-sso setup ecs ssl --self-signed - #1470

Draft
synfinatic wants to merge 21 commits into
mainfrom
self-signed
Draft

Add aws-sso setup ecs ssl --self-signed#1470
synfinatic wants to merge 21 commits into
mainfrom
self-signed

Conversation

@synfinatic

@synfinatic synfinatic commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Summary

The ECS Server's TLS story was a dead end: docs told users the AWS SDKs "do NOT support self-signed certificates or private CA's," and setup ecs ssl only accepted a certificate from a public CA — which no public CA will issue for localhost/loopback/169.254.170.2.

Tracing the actual SDK sources shows this is stale for every SDK except Python/botocore:

  • Go SDK v2, Node, Java, .NET all honor the OS trust store (or NODE_EXTRA_CA_CERTS for Node), so a private CA trusted once per machine/runtime makes ECS Server TLS work.
  • Python/botocore is the one real gap: its container-credentials fetcher hardcodes verification against certifi/its own vendored cacert.pem, ignoring both AWS_CA_BUNDLE and the OS trust store (tracked upstream: aws/aws-cli#9016). The printed trust instructions call this out explicitly, along with an installation-method-agnostic workaround (Homebrew/pipx/the official installer each bundle their own isolated Python, so a bare python3 on $PATH is often the wrong interpreter).

This adds aws-sso setup ecs ssl --self-signed, which generates (or reuses) a local CA (new internal/certutil package, stdlib only, P-256 ECDSA, 10-year validity) and prints per-runtime trust instructions (macOS/Linux/Windows OS store, Node, JVM, .NET, Python/AWS CLI caveat). The CA private key is the only thing persisted, in the SecureStore, mirrored across the keyring, 1Password, and JSON backends.

The leaf certificate is never persisted

An earlier revision of this PR also generated a leaf certificate up front and stored it in the SecureStore, rotated by manually rerunning --self-signed. That's been replaced with a stronger model: the leaf is minted fresh, in memory, at whichever CLI invocation is the natural "about to start" moment — native aws-sso ecs server startup, or aws-sso ecs docker start/write-config right before the container starts (which already has to run before every docker compose up, since the container deletes its security file after one read — so this costs nothing new operationally). Validity dropped from 397 days to 30 since there's nothing left to rotate by hand, and custom SAN support (--san) was dropped since the fixed defaults already cover every documented use case.

Because every leaf mint now touches the CA private key (previously only --self-signed/--rotate-ca did), added best-effort memory scrubbing (certutil.ZeroSecret for the PEM string, zeroPrivateKey for the parsed scalar) at every mint call site — explicitly defense-in-depth, not a secure-erase guarantee, since Go's string immutability, a moving GC, and the SecureStore backends' own internal buffers all limit what's reachable to zero.

setup ecs ssl narrows to CA-only management: --self-signed (generate/reuse the CA), --print-ca, --delete, --rotate-ca. ecs_client_cmd.go (and the e2e/polling helpers) validate via the CA rather than pinning an exact leaf, since the served leaf is now minted independently on each run.

Test plan

  • go build ./..., go vet ./..., gofmt -l .
  • go test ./internal/certutil/... ./internal/storage/... — CA/leaf generation (30-day leaf validity), chain verification via x509.Verify, ZeroSecret/zeroPrivateKey scrubbing, CA-only storage round-trips across all three backends
  • go test ./cmd/aws-sso/... (unit) — native ecs server and ecs docker start/write-config each mint a fresh leaf from the stored CA (chain-verified, non-identical across runs), fail fast on a malformed CA
  • go test -tags e2etests ./cmd/aws-sso/... — full CA generate/reuse/print-ca/delete cycle, a real ServeTLS chain-of-trust HTTPS request against a client trusting only the CA (and a negative case against an unrelated CA), and ecs server minting its own leaf from a directly-seeded CA
  • npx markdownlint-cli2 on changed docs
  • Manual smoke test: generated a CA, started ecs server twice and confirmed the served leaf's serial changes on restart while the CA fingerprint stays fixed, ran ecs docker write-config and confirmed a freshly minted leaf lands in the security file, confirmed --delete clears the CA
  • golangci-lint run — currently panics in this environment on a pre-existing go1.26/go1.27 toolchain mismatch (reproduces identically on main, unrelated to this change)

🤖 Generated with Claude Code

https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh

Comment thread docs/ecs-threats.md Outdated
@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.86667% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.30%. Comparing base (1b00d6c) to head (7905c01).

Files with missing lines Patch % Lines
internal/certutil/certutil.go 87.97% 8 Missing and 8 partials ⚠️
cmd/aws-sso/setup_cmd.go 92.16% 2 Missing and 2 partials ⚠️
internal/ecs/security.go 96.16% 1 Missing and 1 partial ⚠️
internal/ecs/server/server.go 92.86% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1470      +/-   ##
==========================================
+ Coverage   86.04%   87.30%   +1.27%     
==========================================
  Files          77       79       +2     
  Lines        4971     5228     +257     
==========================================
+ Hits         4277     4564     +287     
+ Misses        470      448      -22     
+ Partials      224      216       -8     
Flag Coverage Δ
unittests 87.30% <93.87%> (+1.27%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
cmd/aws-sso/ecs_server_cmd.go 98.45% <100.00%> (+45.12%) ⬆️
cmd/aws-sso/setup_cmd_ecs_ssl_trust.go 100.00% <100.00%> (ø)
internal/ecs/server/healthcheck.go 100.00% <100.00%> (+2.23%) ⬆️
internal/predictor/predictor.go 89.29% <100.00%> (+0.10%) ⬆️
internal/storage/json_store.go 93.19% <100.00%> (ø)
internal/storage/keyring.go 93.71% <100.00%> (ø)
internal/storage/onepassword.go 96.24% <100.00%> (ø)
internal/ecs/server/server.go 97.83% <92.86%> (+4.50%) ⬆️
internal/ecs/security.go 97.11% <96.16%> (+6.00%) ⬆️
cmd/aws-sso/setup_cmd.go 94.12% <92.16%> (+19.76%) ⬆️
... and 1 more

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1b00d6c...7905c01. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It generates a long-lived private CA and instructs users to add it to their OS system trust store, a security-sensitive change that warrants final human review.

Pull request overview

This PR makes ECS Server TLS practical by adding aws-sso setup ecs ssl --self-signed, which generates a local private CA plus a short-lived leaf certificate, stores both in the SecureStore, and prints per-runtime trust instructions. It reframes the previous documentation stance (that self-signed/private CAs are unusable) to reflect that only Python/botocore truly can't trust a private CA for this endpoint. It fits into the existing setup ecs ssl command and the three secure-storage backends.

Changes:

  • New stdlib-only internal/certutil package that generates a long-lived CA and a 397-day leaf (with default SANs for localhost/loopback/169.254.170.2), plus fingerprinting.
  • SecureStorage gains CA key/cert persistence across all three backends (keyring, 1Password, JSON), with matching tests.
  • setup ecs ssl gains --self-signed, --print-ca, --rotate-ca, and --san, plus printed OS/runtime trust instructions and refreshed docs.
File summaries
File Description
Makefile Bumps project version 2.3.2 → 2.4.0.
internal/certutil/certutil.go New CA/leaf generation, SAN merging, and fingerprint helpers.
internal/certutil/certutil_test.go Unit tests for CA/leaf generation, chain verification, SANs, fingerprint.
internal/storage/secure_store.go Adds CA key-pair methods to the SecureStorage interface.
internal/storage/keyring.go Implements CA key-pair storage and adds StorageData fields.
internal/storage/onepassword.go Implements CA key-pair storage (reuses StorageData).
internal/storage/json_store.go Implements CA key-pair storage and adds JsonStore fields.
internal/storage/*_test.go Round-trip tests for CA key-pair storage per backend.
cmd/aws-sso/setup_cmd.go New flags and self-signed/print-ca/rotate flow; CA export to ~/.aws-sso/ecs-ca.pem.
cmd/aws-sso/setup_cmd_ecs_ssl_trust.go Renders per-runtime trust instructions.
cmd/aws-sso/setup_cmd_test.go, setup_cmd_e2e_test.go Unit + e2e coverage incl. real chain-of-trust HTTPS.
docs/ecs-server.md, docs/ecs-threats.md, docs/remote-ssh.md Documentation updates for the new flow.
Review details
  • Files reviewed: 17/17 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmd/aws-sso/setup_cmd_ecs_ssl_trust.go Outdated
synfinatic added a commit that referenced this pull request Aug 29, 2026
Adds targeted unit tests for the error/edge-case branches Codecov flagged
on PR #1470: certutil's CA/key parsing failures (invalid PEM, unparsable
key, non-ECDSA key), setup_cmd's Store-error and filesystem-error paths
in loadOrGenerateCa/runSelfSigned/printCaAndInstructions (via a small
errStore test double), and direct tests of warnIfCertExpiringSoon using
flexlog's test logger to assert on the actual warning message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh
@synfinatic
synfinatic requested a balanced review from Copilot August 30, 2026 01:14
synfinatic and others added 12 commits August 29, 2026 18:20
The ECS Server's TLS story was a dead end: docs told users the AWS SDKs
"do NOT support self-signed certificates or private CA's," and
`setup ecs ssl` only accepted a certificate from a public CA -- which no
public CA will issue for localhost/loopback/169.254.170.2.

Tracing the actual SDK sources shows this is stale for every SDK except
Python/botocore: Go SDK v2, Node, Java, and .NET all honor the OS trust
store (or NODE_EXTRA_CA_CERTS for Node), so a private CA trusted once per
machine/runtime makes ECS Server TLS work everywhere except the AWS CLI.
botocore's container-credentials fetcher hardcodes verification against
certifi/its own vendored bundle, ignoring both AWS_CA_BUNDLE and the OS
trust store (tracked upstream: aws/aws-sdk#9016) -- the trust instructions
call this out explicitly with an installation-method-agnostic workaround.

This adds `--self-signed`, which generates a local CA + leaf certificate
via a new internal/certutil package, stores both in the SecureStore
(mirroring the existing leaf-only storage across the keyring, 1Password,
and JSON backends), and prints per-runtime trust instructions. The CA
private key is retained and reused across reruns, so trust only needs to
be established once per machine -- rerunning `--self-signed` rotates only
the short-lived leaf. `--print-ca` re-displays the instructions on a new
machine without regenerating anything, and `--rotate-ca` forces a fresh
CA when actually needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh
The BYO-certificate path was already labeled experimental and gated
behind `--force` (issue #936). Now that `--self-signed` exists and is
the recommended, fully-supported path, there's no reason to keep the
manual load-your-own-cert flow around -- it only adds surface area for
a case `--self-signed` already covers (private CA for localhost/loopback,
which is the only reason a public CA can't be used here in the first
place).

`--self-signed` is now the default action of `setup ecs ssl` when no
other flag is given, since it's the only certificate-issuing action
left.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh
It's a legitimate, occasionally-needed operation (leaf SANs changing
in a way that needs a fresh CA, suspected key compromise, etc.), not
an internal/debug-only flag, so it should show up in --help.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh
`--self-signed`/`--print-ca` printed a long block of per-OS/per-runtime
trust instructions on every run. Move the full instructions (including the
Python/AWS CLI caveat) into docs/ecs-server.md and have the CLI print only
a short summary (CA path, fingerprint, SANs) plus a link to the docs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh
Add certutil.NotAfter and check both the leaf and CA certificates when
'ecs server' starts with SSL/TLS enabled, warning if either expires within
30 days (or already has) and naming the fix ('--self-signed' for the leaf,
'--rotate-ca' for the CA). The CA check is skipped in --docker mode, since
the mounted security config file only carries the leaf cert/key, not the CA.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh
Adds targeted unit tests for the error/edge-case branches Codecov flagged
on PR #1470: certutil's CA/key parsing failures (invalid PEM, unparsable
key, non-ECDSA key), setup_cmd's Store-error and filesystem-error paths
in loadOrGenerateCa/runSelfSigned/printCaAndInstructions (via a small
errStore test double), and direct tests of warnIfCertExpiringSoon using
flexlog's test logger to assert on the actual warning message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh
--print-ca's help text and docs said it prints the CA certificate, but
printCaAndInstructions only ever printed the path/fingerprint/trust
instructions, never the PEM itself -- unlike --print, which does print
the leaf certificate. printCaAndInstructions now takes a printCert bool
so --print-ca prints the CA PEM (matching --print's behavior) while
--self-signed's internal call keeps its existing summary-only output.

fix: export the ECS CA cert under config.ConfigDir() instead of a fixed ~/.aws-sso path

EcsCaExportPath was hardcoded to "~/.aws-sso/ecs-ca.pem". config.ConfigDir()
treats the mere existence of ~/.aws-sso as a directory as a signal to switch
the whole tool into legacy (non-XDG) mode, so on a fresh XDG install this
would silently create that directory and flip every other aws-sso config/
store/cache file over to the legacy path on the next run. Write the CA
export next to the rest of aws-sso's config/store files instead, using
whichever location config.ConfigDir() actually resolves to.
The CA (10-year, ECDSA P-256) is still generated once and persisted in the
SecureStore -- that's what avoids re-trusting anything on every rerun. The
leaf it signs no longer is: it's now minted fresh, in memory, wherever it's
about to be used -- native `ecs server` startup, or `ecs docker start` /
`write-config` right before the container starts -- and validity drops from
397 days to 30 since there's nothing left to rotate by hand. Custom SAN
support (`--san`) is dropped; the default SANs already cover every
documented use case.

Because every leaf mint now touches the CA private key (not just the rare
`--self-signed`/`--rotate-ca` run), add best-effort memory scrubbing:
certutil.ZeroSecret for the PEM string, zeroPrivateKey for the parsed scalar
in GenerateLeaf. Both are explicitly defense-in-depth, not a secure-erase
guarantee. Fetching the CA key from a SecureStore backend returns its
cached string directly (shared backing array), so every mint call site
clones it via strings.Clone before mutating/zeroing it -- zeroing an
aliased string would otherwise corrupt the store's own in-memory copy (and
crash on read-only-backed literals).

setup_cmd.go's EcsSSLCmd narrows to CA-only management (Delete, PrintCa,
SelfSigned, RotateCa); its reuse path no longer reads the CA key at all.
ecs_client_cmd.go switches to CA-based trust so it validates whichever leaf
is actually being served rather than pinning one that no longer persists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh
Rewrite the ECS Server SSL section for the new certificate lifecycle: the
CA is generated/reused via --self-signed and is the only thing that's
persisted; a 30-day leaf is minted automatically in memory wherever it's
needed and never rotated or printed by hand. Drop the --print/--san
documentation, narrow the expiry-warning paragraph to CA-only, and note the
long-running-compose-without-restart limitation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh
ecs-threats.md proposed a hosted CSR-signing web service that was never
built; --self-signed's local CA solved the underlying problem differently,
leaving the doc's Solution/Attacks/Suggestions sections describing a design
that won't be pursued. It was only reachable via its own mkdocs nav entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh
…SSL test flake

TestEcsSSLCmdRun_PrintCa_EnsureDirExistsError and ...WriteFileError used
assert.Error(t, err) (a soft assertion) immediately followed by
err.Error(), so on the rare occasion err came back nil the very next
line dereferenced a nil interface and panicked, crashing the whole test
binary instead of just failing the one test -- this is what took down
the full `make coverage` run in CI. Switch both to require.Error so a
recurrence fails cleanly. Also derive the CA export path via
config.ConfigDir() instead of manually reconstructing it, so the test
can never target a different directory than what printCaAndInstructions
actually resolves at call time.

Root cause of the CI-only failures: config.ConfigDir() prefers
XDG_CONFIG_HOME over $HOME, with a heuristic that only re-derives it
from a changed $HOME when the old XDG_CONFIG_HOME exactly matches the
old $HOME/.config. GitHub Actions' runners export a real
XDG_CONFIG_HOME (e.g. /home/runner/.config), so tests that override
$HOME via t.Setenv without also touching XDG_CONFIG_HOME still resolve
config.ConfigDir() to the real, un-isolated directory. One of the
errpath tests then deletes and overwrites that real shared directory,
corrupting it for every other test in the process (the cascade of
"storage.lock: not a directory" failures seen in CI).

Reproduced locally by setting XDG_CONFIG_HOME=$HOME/.config before
running the suite -- confirmed the same corruption occurs, and that
clearing XDG_CONFIG_HOME alongside the $HOME override (t.Setenv can't
express "unset", so this is a manual save/unset/restore) fixes it, by
adding a shared unsetXDGConfigHomeForTest helper and calling it
everywhere a test overrides $HOME for config.ConfigDir() isolation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It is a broad, security-sensitive change (private-CA generation, persisted key material, TLS trust) that warrants human review, and it also contains documentation inaccuracies that should be corrected first.

Review details

Suppressed comments (1)

docs/ecs-commands.md:41

  • These flag docs are out of sync with the implemented command. The current EcsSSLCmd only defines Delete, PrintCa, SelfSigned, and RotateCa (cmd/aws-sso/setup_cmd.go), so --san (line 36) and --print (line 40) no longer exist — --san was dropped and there is no leaf-printing flag. Also, --self-signed no longer "issue[s] a new leaf certificate"; it only generates/reuses the CA (the leaf is minted in memory at server/docker startup). Please remove the stale flags and correct the wording.
* `--self-signed` -- Generate (or reuse) a local CA and issue a new leaf certificate for the
  ECS Server. This is the default action when no other flag is given.
* `--san` -- Additional DNS name or IP address to include in the self-signed leaf certificate (repeatable)
* `--rotate-ca` -- Force generation of a brand new CA instead of reusing the existing one (requires
  re-trusting on every client)
* `--print-ca` -- Prints the local self-signed CA certificate and re-prints trust instructions
* `--print` -- Prints the current SSL (leaf) certificate
* `--delete` -- Disables SSL and deletes the current SSL certificate/private key and CA from the Secure Store
  • Files reviewed: 27/27 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread docs/ecs-commands.md Outdated
Comment thread docs/ecs-server.md Outdated
Comment thread internal/certutil/certutil.go Outdated
synfinatic and others added 6 commits August 30, 2026 07:51
… key

Two hardening fixes to the self-signed CA code.

Name constraints: GenerateCA now emits a critical nameConstraints extension
(RFC 5280) permitting only localhost, 127.0.0.0/8, ::1/128, and
169.254.170.2/32. This matters because the documented setup installs this CA
as a trust root -- OS trust store, JVM cacerts, NODE_EXTRA_CA_CERTS. Without
constraints, anyone who obtained the CA private key could forge a valid
certificate for any name at all (sts.amazonaws.com, a bank, a corporate SSO
endpoint), turning a localhost convenience CA into a universal MITM
capability for that machine. Constrained, a leaked key can only forge certs
for the endpoints the ECS Server itself listens on.

Both a DNS and an IP constraint are set deliberately: under RFC 5280 a name
type carrying no constraint of that type is entirely unconstrained, so
permitting only DNS names would have left IP SANs wide open. The extension is
marked critical so a validator that does not understand nameConstraints
rejects the CA outright instead of silently treating it as unconstrained.

Key scrubbing: zeroPrivateKey used big.Int.SetInt64(0), which only reslices
the scalar's backing word array to length zero -- every word of the secret
survives intact in memory behind it. It never scrubbed anything. Overwrite
the words through the slice, then normalize via SetBits.

Tests cover both. The name-constraint tests assert the extension is present
and critical, that every default leaf SAN still verifies against the CA (the
constraints must stay a superset of the SANs or the CA cannot sign its own
leaves), and that certificates forged with the real CA key for foreign names
fail verification. TestZeroPrivateKey now holds the backing array and asserts
it is actually zeroed; its previous key.D.Sign() check passed against the
broken implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDJ1csupGfLzVyuy2qEfYh
Reports whether a leaf was issued by a given CA and is still valid for server
authentication.  Callers that hold an already-persisted leaf need this to tell
"still good, reuse it" from "the CA was rotated out from under it, re-mint".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpzTc3khofuwL3uLRH3oE8
Serve() was writing the certificate and private key to a temp directory purely
because http.Server.ServeTLS takes file paths.  ServeTLS only reads those paths
when TLSConfig has no certificate of its own, so loading the PEM with
tls.X509KeyPair and passing empty paths keeps the key off disk entirely.

Also sets MinVersion to TLS 1.2, which the file-based path never specified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpzTc3khofuwL3uLRH3oE8
An expired leaf breaks every TLS client regardless of what credentials are
loaded, and the container cannot fix it itself.  Returning 503 surfaces that
through `docker ps` and blocks `depends_on: condition: service_healthy`,
instead of letting dependent containers start and fail with opaque TLS errors.

The expiry is parsed once in NewEcsServer rather than per probe.  A parse
failure there is not fatal -- Serve() validates the pair properly and fails
loudly -- so the healthcheck just treats an unparseable cert as unexpired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpzTc3khofuwL3uLRH3oE8
The CA certificate was exported to ecs-ca.pem under the config directory on
every --self-signed run, so the trust instructions could name a path.  That put
a second copy of the CA on disk that nothing cleaned up, and left the stored
copy and the exported one able to drift after a rotation.  The CA now lives
only in the secure store.

--print-ca writes the PEM to stdout and nothing else, so it can be redirected
straight to a file the trust-store commands consume.  The fingerprint and the
docs link would corrupt that redirect, so they moved to the --self-signed
summary instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpzTc3khofuwL3uLRH3oE8
`docker compose` owns its own container lifecycle, so the ECS Server has to
come back up with the same security posture after a restart or recreation
without aws-sso being in the loop.  Three changes make that work.

Rename `ecs docker write-config` to `ecs docker secrets`.  The old name
described the mechanism and read like it wrote config.yaml; what it emits is a
bearer token and a private key.  It never shipped, so there is no alias.

Persist the leaf certificate and refresh it as it ages.  `ecs docker secrets`
writes the leaf into the security file rather than relying on a fresh one per
start, and reuses a still-valid one so a running container's certificate is not
invalidated by a re-run.  The host-side `ecs load`/`list`/`profile` commands top
up an aging leaf, so a compose deployment does not silently lapse once the
30-day validity runs out.

Add `ecs --secrets-dir` (env AWS_SSO_ECS_SECRETS_DIR).  The directory was fixed
at <config dir>/ecs, and compose.yaml advertised an AWS_SSO_ECS_DIR variable
the binary never read -- the two could not be pointed at the same place.  The
flag sits on the `ecs` parent so one setting covers writing the files,
bind-mounting them, and refreshing the leaf.  Relative paths are made absolute
because docker bind mounts reject a relative source.

That last change forces a split in internal/ecs: once the host side takes a
directory, the ECSFileMode dispatch no longer holds a common signature, so
SecurityFilePath/OpenSecurityFile become OpenHostSecurityFile(dir) plus
ContainerSecurityFilePath/OpenContainerSecurityFile.  Only the container half
still needs a path override for tests -- its path is baked into the image and
cannot be created on the host -- so host-side tests pass a temp dir instead of
mutating a package global.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpzTc3khofuwL3uLRH3oE8
Adds compose.yaml as a working example, describes the CA-persists /
leaf-refreshed model, and covers `ecs docker secrets`, `--secrets-dir`, and
AWS_SSO_ECS_SECRETS_DIR.

Also audited every `ecs` subcommand's documented flags against the binary.
That turned up an `--image` default naming an image that does not exist
(`...-ecs-version`), a `--slotted` flag documented on `ecs profile` which does
not accept it, and missing entries for `--sts-refresh`, `--bind-ip`, `--port`,
`-d/--default`, `ecs docker stop --version`, and the AWS_SSO_ECS_SERVER
environment variable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpzTc3khofuwL3uLRH3oE8
synfinatic and others added 2 commits August 31, 2026 12:07
The KeyPair doc comment still named SaveEcsSslKeyPair, which this branch
renamed to SaveEcsCaKeyPair. Only SaveEcsCaKeyPair validates the key now,
so name just that one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LDKQDAmBV12j7zi2LvFP8E
… for --docker

Empty PrivateKey/CertChain/BearerToken in the security file used to mean
either "explicitly disabled" or "never configured" indistinguishably, so
`ecs server --docker` could only fail closed when the file itself was
missing -- an empty-but-present file silently started serving credentials
with no TLS or HTTP Auth. ECSSecurity now records DisableSSL/DisableAuth
at write time, so the Docker server can fail closed on an unexplained gap
and start open only when the file says so. --docker no longer reads its
own --disable-auth/--disable-ssl flags at all; ecs docker secrets is now
the sole place to request that posture.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LDKQDAmBV12j7zi2LvFP8E
@synfinatic
synfinatic marked this pull request as draft September 3, 2026 01:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants