transport-security-setup
Decide onde o TLS termina, no edge ou no Tomcat embutido, e escreve a configuração correspondente e o teste que a prova.
transport-security-setup, seção a seção
As seções do próprio SKILL.md da skill, em trechos. Passe o mouse ou clique numa delas para ver quais regras do projeto, templates e arquivos de references/ ela cita. As seções também são chips: clique num para ir até ela.
## 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… cortado aqui, o resto está no arquivo
### 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
O que lê, escreve e recusa
O contrato da própria skill, do SKILL.md, no texto original.
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.
Fonte: SKILL.md § Contract, no nerviz e4cae27.
O que o guard faz com ela
build é a classe da skill, e cada skill dela tem território próprio. Quem chama decide qual território vale: sob o project-initializer, o do agent; da thread principal, nenhuma chamada passa. O texto à direita é o do próprio hook.
write_allow da classe 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 qualquer caminho fora desta lista, quando a própria skill abre a faseuma chamada da thread principal com o /init-project aberto
A recusa sai com código 2 e a mensagem volta para o modelo.
Encadeada pelo project-initializer, as escritas dela levam o agent_type do agent e são julgadas por agent_classes.driver, **. Esta lista vale quando a skill roda num prompt próprio.
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.
Onde ela fica no trilho
Cada card é um passo do /init-project. A entrevista roda na sessão principal; os passos com fundo colorido rodam dentro do project-initializer; o git-publish volta para a sessão principal, depois do audit genesis. Passos tracejados só rodam quando se aplicam.