Skip to content
Back to skills

Transport Security Setup

ASecurity

Decides where TLS terminates for a generated or adopted Spring Boot project — at the edge (ingress, load balancer, Caddy) or in the embedded Tomcat, with mutual TLS as a recorded variant — and writes the matching configuration: forwarded headers and trusted proxies, or an SSL bundle with reload and profile-gated TLS; HTTP/2 on; HSTS with exactly one writer; the integration test that proves it on a real server; a local Caddy edge through docker-architect. Use when the request involves HTTPS, T...

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 6, 2026
ai-agentsrustgojavashellbashexpressspringdockerkubernetestesting

Works with

  • cli

Security analysis

A100/100

Pro scans all 7 files and shows the line behind each finding

Scanned October 6, 2026

npx -y skills add nerviz-ai/nerviz --skill transport-security-setup --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Transport Security Setup?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Transport Security Setup
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nerviz-ai-transport-security-setup/badge)](https://www.skillsdirectory.com/skills/nerviz-ai-transport-security-setup)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: transport-security-setup
description: >
  Decides where TLS terminates for a generated or adopted Spring Boot project — at the edge
  (ingress, load balancer, Caddy) or in the embedded Tomcat, with mutual TLS as a recorded
  variant — and writes the matching configuration: forwarded headers and trusted proxies, or an
  SSL bundle with reload and profile-gated TLS; HTTP/2 on; HSTS with exactly one writer; the
  integration test that proves it on a real server; a local Caddy edge through docker-architect.
  Use when the request involves HTTPS, TLS, certificates, HTTP/2, h2c, HSTS, X-Forwarded-Proto,
  "behind a proxy", "configure https", or when project-bootstrap or arch-adopt finds a project
  whose root CLAUDE.md has no Transport line.
allowed-tools: Read, Write, Edit, Grep, Glob, AskUserQuestion, Skill, Bash(grep:*), Bash(./mvnw:*), Bash(./gradlew:*)
model: sonnet
effort: low
---

# Transport Security Setup

Decides once, per project, how a request reaches it over TLS, and writes what that decision
needs. One question decides the shape: **where does TLS terminate?** At the edge → the
application holds no key and learns the scheme from the edge's forwarded headers. In the
embedded server → the application loads a mounted certificate through an SSL bundle and serves
8443. Every rule this skill applies is `@.claude/rules/transport-security.md`; every rule about
key material is `@.claude/rules/secrets.md`.

**Entry rule: decide once.** The root `CLAUDE.md` already has a `**Transport:` paragraph →
report "already decided", quote it, and stop. Changing topology is a new decision: the user
deletes the paragraph and runs `/transport-security-setup` again, which then rewrites the
configuration it owns. When the paragraph is missing but `application.yml` already holds
`server.ssl`, `spring.ssl.bundle` or `forward-headers-strategy`, the project decided by hand:
report the lines found, ask which topology they implement, and write only the paragraph — never
rewrite a configuration this skill did not write.

**No key, certificate or password is written, anywhere.** Not in `application.yml`, not under
`src/test/resources`, not "for local use". The production files are mounted at run time and
their paths come from the environment; the test generates its own certificate when it runs.

## 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

```bash
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.xml` at the root → `maven`; `build.gradle` → `gradle`. The main module is the one with
  `contains_main: true` in the active blueprint; its `src/main/resources/application.yml` is
  the file this skill merges into.
- `spring-boot-starter-security` present → the security filter chain owns HSTS, and step 4 is
  skipped. Absent → step 4 writes the filter.
- `spring-boot-starter-jetty` present → topology B and C are not offered: Jetty does not reload
  a bundle, and the certificate would expire with the application running. Say so in the
  question.

### 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
  behave on the developer's machine as behind the production edge?" — *Yes (Recommended)* /
  *No — local runs plain HTTP*.
- B or C — two questions in the same call:
  1. **Local profile**: "Should the `local` profile skip HTTPS?" — *Yes — local and docker run
     plain HTTP on 8080 (Recommended)* / *No — local needs a certificate too (mkcert)*.
  2. **Reason** for not terminating at the edge, recorded in the paragraph — *No proxy in
     front* / *Compliance requires TLS up to the process* / *Clients reach the process
     directly*. Topology C also records that the client-certificate authentication mechanism is
     designed later, by `security-architect`, when a use case's access rule names a machine
     caller with a certificate.

### 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):

```bash
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 —

```bash
grep -rl "class TestcontainersConfiguration" --include='*.java' src/test/ */src/test/ 2>/dev/null
```

— every `@SpringBootTest` class of the test you write carries
`@Import(TestcontainersConfiguration.class)`. In `ForwardedHeadersIT` that means **each nested
class**: `@NestedTestConfiguration(OVERRIDE)` drops the enclosing class's configuration, an import
included. Without it, a project with JPA or Flyway starts the context against `localhost:5432` —
connection refused on a clean machine, or another project's database where one is running
(lessons-learned-020 § 2). The exemplars don't carry the import: they are compiled in CI against a
project with no datasource, where the class does not exist.

### 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) and `docker/caddy/Caddyfile`, chained from
  `transport-security-setup` — no engine question".
- B or C → "`Dockerfile`: the image also listens on 8443 (`EXPOSE 8080 8443`), chained from
  `transport-security-setup` — no service to add".

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_PROXIES` to the edge's addresses (CIDR list);
  the default trusts every private range. With the local edge: `docker compose up -d edge`, then
  `docker compose exec edge caddy trust` once, and open `https://localhost:8443`.
- **B / C** — mount the certificate and key (PEM, PKCS#8) as files and set `TLS_CERTIFICATE` and
  `TLS_PRIVATE_KEY` to their `file:` paths; renew them with an ACME client or the platform, never
  by hand. A Kubernetes secret is mounted as a directory, never with `subPath`. Probes read
  `/actuator/health/readiness` and `/liveness`. Local profile *No* → generate a local pair with
  `mkcert localhost` outside the repository and point the two variables at it.
- **C** — the bundle's truststore holds only the private CA that issues client certificates, from
  `TLS_CLIENT_CA`.

## Contract

**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.

Files in this skill

  • SKILL.md12.4 KB
  • templates/CLAUDE-transport.md.example969 B
  • templates/ForwardedHeadersIT.java.example3.5 KB
  • templates/HstsHeaderFilter.java.example2.5 KB
  • templates/TransportSecurityIT.java.example6.1 KB
  • templates/application-tls-direct.yml.example3.8 KB
  • templates/application-tls-edge.yml.example2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.