Concrete configuration that shrinks an API's attack surface at the edge and in the runtime — transport, CORS, limits, headers, and egress.
Scanned 9/10/2026
Install to Claude Code
npx -y skills add snoodleboot-io/prompticorn --skill minimal --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Minimal?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/snoodleboot-io-minimal-prompticorn)More formats (shields.io, HTML) on the badges page.
# API Security Hardening (Minimal)
## Purpose
Concrete configuration that shrinks an API's attack surface at the edge and in the runtime — transport, CORS, limits, headers, and egress.
## Core Techniques
### 1. Terminate TLS With a Modern Profile
```nginx
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off; # let TLS 1.3 clients choose
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
```
Drop TLS 1.0/1.1 outright. Add `preload` to HSTS only once every subdomain — including the legacy admin host — serves HTTPS; getting off the preload list takes months.
### 2. Configure CORS Explicitly, Never Reflectively
```python
# ❌ Reflecting Origin with credentials = any site can call your API as the user
headers["Access-Control-Allow-Origin"] = request.headers["Origin"]
headers["Access-Control-Allow-Credentials"] = "true"
# ✅ Static allowlist, exact string match
if origin in {"https://app.example.com", "https://admin.example.com"}:
headers["Access-Control-Allow-Origin"] = origin
headers["Vary"] = "Origin"
```
Browsers reject `Allow-Origin: *` when credentials are sent; teams "fix" that by reflecting the origin, which is strictly worse. CORS is also not authorization — it constrains browsers only, never curl.
### 3. Bound Every Request Dimension
| Limit | Typical value | Blocks |
|---|---|---|
| Body size | 1 MB (10 MB for uploads) | Memory exhaustion |
| Header size | 8 KB | Header stuffing |
| JSON nesting depth | 32 | Parser stack blowup |
| Page/array size | `limit <= 100` | Unbounded result sets |
| Request timeout | 10–30 s | Slowloris, hung upstreams |
Set `client_max_body_size 1m;` at the proxy *and* the same limit in the app — anything that reaches the pod directly bypasses the edge.
### 4. Rate-Limit in Layers
```nginx
limit_req_zone $binary_remote_addr zone=perip:10m rate=20r/s;
limit_req zone=perip burst=40 nodelay;
```
Edge/IP limits absorb floods; per-principal quotas inside the app protect expensive endpoints. Always return `429` with `Retry-After` — without it, clients retry immediately and convert a limit into an outage.
### 5. Send API-Appropriate Headers
```
Content-Type: application/json
X-Content-Type-Options: nosniff
Cache-Control: no-store
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
```
Most HTML-oriented headers are noise on a JSON API; `nosniff` and `no-store` are not. `no-store` on authenticated responses keeps tokens and PII out of shared caches.
### 6. Constrain Egress and Runtime
Deny-by-default egress NetworkPolicy, `runAsNonRoot: true`, read-only root filesystem, all capabilities dropped. An SSRF or RCE that cannot dial out or reach the cloud metadata endpoint is a contained incident instead of a breach.
## Warning Signs
- `Access-Control-Allow-Origin` echoing the request's `Origin` header
- No body-size limit, or one configured only at the CDN
- `429` responses with no `Retry-After` — or no 429s at all under load testing
- TLS 1.0/1.1 or 3DES still negotiable (`nmap --script ssl-enum-ciphers -p 443 host`)
- Wildcard origin allowlists matched with `endswith("example.com")` — `evilexample.com` passes
- Containers running as uid 0 with unrestricted egress
- `/metrics`, `/debug/pprof`, or actuator endpoints bound to 0.0.0.0
- `Server:` / `X-Powered-By` headers advertising exact framework versions
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!