transport-security-setup
Decides where TLS terminates, at the edge or in the embedded Tomcat, and writes the matching configuration and the test that proves it.
transport-security-setup, section by section
The sections of the skill's own SKILL.md, excerpted. Hover or click one to see which project rules, templates and references/ files it cites. The sections are also chips: click one to jump to it.
## Why this is a skill (Form 1) and not Form 2
Form 1: project-bootstrap and arch-adopt chain it by name, and a skill with disable-model-invocation: true cannot be called through the Skill tool — the reason sonarqube-setup stays model-invocable. Axis 2 of the interview — two callers, one of which runs inside a project where project-bootstrap does not exist — made it a skill of its own; the closest rejected form was a transport block inside security-architect, a design skill that writes docs only and runs per use case, so a project with no protected endpoint would never decide transport. Record: @.claude/decisions/0122-transport-security-setup-and-secrets.md.
Runs on sonnet with effort: low: one interview with fixed options, then edits per answer (@.claude/decisions/0080-procedural-skills-on-sonnet.md).
## Procedure
Project directory. When the caller's one-line context names project: <absolute path>, that directory is the project root, not the session's: read and write every path of this procedure under it, and run every shell command as (cd "<path>" && …). When this skill chains docker-architect, pass project: <path> on in its context.
### 1 · Detect
grep -n "^\*\*Transport:" CLAUDE.md 2>/dev/null
grep -rnE "server\.ssl|ssl:|bundle:|forward-headers-strategy" --include='application*.yml' --include='application*.properties' . 2>/dev/null | grep -v /target/
grep -nE "spring-boot-starter-(security|jetty)" pom.xml build.gradle */pom.xml */build.gradle 2>/dev/null
**Transport:found → entry rule: report and stop.pom.xmlat the root →maven;build.gradle→gradle. The main module is the one withspring-boot-starter-securitypresent → the security filter chain owns HSTS, and step 4 isspring-boot-starter-jettypresent → topology B and C are not offered: Jetty does not reload
### 2 · Interview — two `AskUserQuestion` calls
When a caller already passed an answer in its one-line context, don't ask it again.
First call — the topology, with one line of what each costs:
| Option | Means |
|---|---|
| *A · Edge (Recommended)* | An ingress, load balancer or reverse proxy terminates TLS and renews the certificate; the application holds no key and trusts X-Forwarded-* from the edge's network only |
| *B · Direct* | The embedded Tomcat terminates TLS on 8443 with a certificate mounted at run time and reloaded on renewal; the application owns the key |
| *C · Mutual TLS* | B, and every caller presents a client certificate from a private CA; a missing one fails the handshake, never a 401 |
| *C′ · Service mesh* | The mesh sidecar terminates TLS; the application is configured as in A |
Second call, depending on the first:
- A or C′ — local edge: "Run a local Caddy edge in docker-compose, so HTTPS, HTTP/2 and HSTS
- B or C — two questions in the same call:
### 3 · Write the configuration
Merge into the main module's application.yml — into its existing roots, never a second server: or spring: key in the same document:
| Topology | Exemplar | Adjust | |
|---|---|---|---|
| A, C′ | templates/application-tls-edge.yml.example | nothing | |
| B | templates/application-tls-direct.yml.example | the first block into the existing roots, the --- document appended at the end. Local profile *No* → the expression becomes `"!docker & (!test | tls)"` |
| C | same as B | also uncomment the bundle's truststore and server.ssl.client-auth: need |
server.http2.enabled: true goes in for every topology.
### 4 · HSTS — only when step 1 found no Spring Security
Write templates/HstsHeaderFilter.java.example into the blueprint's configuration package plus .web (config.web, infrastructure.config.web); in a blueprint with no configuration package (hexagonal, onion, custom), into a web subpackage of the inbound REST adapter. Base package and package names come from the blueprint's packages.map, never from the exemplar.
With Spring Security present, nothing is written: its default header writer already sends the same value, and the filter would only override it.
### 5 · The test
| Topology | Exemplar | Where |
|---|---|---|
| A, C′ | templates/ForwardedHeadersIT.java.example | src/test/java/<base package>/ of the main module |
| B, C | templates/TransportSecurityIT.java.example | same |
For B and C, every @SpringBootTest must run with the test profile, or its context now starts with TLS on and fails reading /etc/tls (@.claude/rules/testing.md § Slices and context requires it anyway):
grep -rL '@ActiveProfiles("test")' $(grep -rl "@SpringBootTest" --include='*.java' src/test/ 2>/dev/null) 2>/dev/null
Add @ActiveProfiles("test") to each file listed. The generator's own *ApplicationTests is usually the only one.
A datasource in the context. When the project has a Testcontainers configuration —
grep -rl "class TestcontainersConfiguration" --include='*.java' src/test/ */src/test/ 2>/dev/null… cut here, the rest is in the file
### 6 · Container — through `docker-architect`
Invoke docker-architect through the Skill tool with a one-line context, and only in these two cases. It owns every service block and the Dockerfile; don't write either here, not even when it looks like a copy.
- A or C′ with a local edge → "service
edge(Caddy) anddocker/caddy/Caddyfile, chained from - B or C → "
Dockerfile: the image also listens on 8443 (EXPOSE 8080 8443), chained from
No docker-compose.yml or Dockerfile at the root → docker-architect stops on its own entry rule; say so in the report.
### 7 · Record the decision
Write the matching paragraph of templates/CLAUDE-transport.md.example into the root CLAUDE.md, placeholders filled, right below the **Bounded context: paragraph (or below the first heading when there is none). That template owns the wording.
### 8 · Verify
When chained by project-bootstrap, skip: its own Verify runs next and covers the test this skill wrote. Otherwise run the project's full build — ./mvnw -B verify or ./gradlew build. A red test is reported with its first failing assertion, never "fixed" by loosening the configuration.
## Report
✅ Transport decided — topology <A edge | B direct | C mutual TLS | C′ mesh>
Configuration . <application.yml path> — <forwarded headers + trusted proxies | SSL bundle `webserver`, reload, 8443, TLS on except <profiles>> · HTTP/2 on
HSTS .......... <Spring Security's default writer | HstsHeaderFilter in <package>>
Test .......... <ForwardedHeadersIT | TransportSecurityIT> — <green in the build | runs in project-bootstrap's Verify> · @ActiveProfiles("test") added to <n files | none needed>
Container ..... <edge (Caddy) added by docker-architect | Dockerfile EXPOSE 8443 by docker-architect | not touched — <reason>>
CLAUDE.md ..... Transport paragraph written
Then the lines the person needs on day one, by topology:
- A / C′ — in each environment, set
TRUSTED_PROXIESto the edge's addresses (CIDR list); - B / C — mount the certificate and key (PEM, PKCS#8) as files and set
TLS_CERTIFICATEand - C — the bundle's truststore holds only the private CA that issues client certificates, from
What it reads, writes and refuses
The skill's own contract, from SKILL.md, in the original wording.
Class
build — the territory is skill_classes.build's override for this skill in @.claude/schemas/extensions.json: the main module's application*.yml, the HstsHeaderFilter.java file, src/test/ (the IT, and the test profile on existing @SpringBootTest classes), and the root CLAUDE.md. ArchHook.java guard enforces it. docker-compose.yml, docker/ and the Dockerfile are deliberately outside it.
Reads
the root CLAUDE.md, the build file, the active blueprint's packages.map, and the main module's application*.yml.
Writes
the transport configuration, the HSTS filter when no security chain exists, one transport IT, and the **Transport: paragraph — the project fact security-architect, http-client-architect and docker-architect read.
Never writes
a certificate, a key, a keystore or a password, in any file.
Delegates
the edge service, the Caddyfile and the Dockerfile port to docker-architect, the single owner of both files. Outbound TLS — a client certificate or a private CA for a call this service makes — is http-client-architect's, never this skill's. The client-certificate authentication mechanism of topology C is security-architect's, designed with the first use case that needs it.
Chained by
project-bootstrap (step 7.5, before Verify) and arch-adopt (step 7.5), and invocable by hand. Travels into the generated project (export.skills.include): arch-adopt runs inside the project, where project-bootstrap does not exist, and needs this skill there.
Source: SKILL.md § Contract, at nerviz e4cae27.
What the guard does with it
build is the skill's class, and each skill of it carries its own territory. Who calls it decides which territory applies: under project-initializer the agent's, from the main thread no call at all. The text on the right is the hook's own.
write_allow of class transport-security-setup
- ✓
src/main/resources/application*.yml - ✓
**/src/main/resources/application*.yml - ✓
src/main/java/**/HstsHeaderFilter.java - ✓
**/src/main/java/**/HstsHeaderFilter.java - ✓
src/test/** - ✓
**/src/test/** - ✓
CLAUDE.md any path outside this list, when the skill opens the phase itselfa call from the main thread while /init-project is open
A refusal exits with code 2 and the message goes back to the model.
Chained by project-initializer, its writes carry the agent's agent_type and are judged by agent_classes.driver, **. This list applies when the skill runs on a prompt of its own.
PreToolUse Skill(transport-security-setup) # agent_type: project-initializer (class driver) guard call exit 0 # a classed agent's call leaves the phase as it was PreToolUse Write(../bank-app/src/main/resources/application.yml) guard write exit 0 # judged by agent_classes.driver, write_allow: **
# phase open: init-project (class orchestrator) PreToolUse Skill(transport-security-setup) guard call exit 2 ❌ `transport-security-setup` is class `build` and `init-project` (class `orchestrator`) is open — a design run does not materialize files. Record what is missing in the partial and in the consolidated spec, finish the run, then invoke `/transport-security-setup` from a prompt of its own.
# in the generated project, on a prompt of its own > /transport-security-setup … UserPromptSubmit audit prompt exit 0 guard prompt exit 0 # phase opened: transport-security-setup (class build) PreToolUse Write(pom.xml) guard write exit 2 ❌ `transport-security-setup` is class `build` — pom.xml is outside its territory. write_allow: src/main/resources/application*.yml, **/src/main/resources/application*.yml, src/main/java/**/HstsHeaderFilter.java, **/src/main/java/**/HstsHeaderFilter.java, src/test/**, **/src/test/**, CLAUDE.md Put the content in the spec as a code block, or finish the run and invoke the skill that owns this path. If the path is legitimately this skill's, widen skill_classes.build in .claude/schemas/extensions.json — retrying will not help.
Where it sits on the rail
Each card is a step of /init-project. The interview runs in the main session; the tinted steps run inside project-initializer; git-publish comes back to the main session, after audit genesis. Dashed steps run only when they apply.