Security considerations¶
SPIRENS's defaults are safe for the common "one box, one domain" case. But there are several surface areas worth understanding explicitly — and a few that surprise even experienced operators.
This page consolidates callouts scattered across the other docs. Read it once before going to production; skim it when you onboard a new operator.
Secret handling¶
What counts as a secret¶
Everything SPIRENS treats as sensitive lives in one of two places:
.envfile — environment variables, read bydocker composesecrets/directory — files mounted into containers as Docker secrets (bcrypt hashes, API tokens, cert private keys)
Both are gitignored. Never commit either. spirens setup generates the
initial set; spirens bootstrap regenerates the ones that are missing.
What lives where¶
| Item | Location | Rotation |
|---|---|---|
CF_DNS_API_TOKEN |
.env, mounted as secrets/cf_api_token |
Regenerate at Cloudflare, paste into .env, restart stack |
| Traefik dashboard password | secrets/traefik_dashboard_htpasswd |
Re-run spirens gen-htpasswd |
REDIS_PASSWORD |
.env |
Blank the var, re-run spirens bootstrap, restart Redis + dweb-proxy |
| Vendor RPC API keys | .env |
Regenerate at vendor, paste, restart eRPC |
| LE account key / certs | letsencrypt/acme.json (mode 0600) |
Delete file to force fresh LE account on next boot |
| Kubo node identity | IPFS volume (peerid) |
Delete IPFS volume — irreversible, loses peer reputation |
| Cloudflare Origin Cert key | secrets/cf_origin.key (if used) |
Regenerate at Cloudflare, rotate manually |
Redacting for support¶
spirens doctor output is safe to paste publicly — it redacts tokens.
docker logs output is not — Traefik and eRPC both occasionally log
request headers that can contain tokens. Grep for Authorization,
token, and your own domain before sharing log bundles.
API token scoping¶
Cloudflare¶
Use a scoped token, never the Global API Key. The exact scopes SPIRENS needs:
Zone.DNS:Editon your specific zone — required for ACME DNS-01 and for the optional DDNS / dns-sync modules.Zone:Readon your specific zone — required to look up the zone ID.Zone.Zone Settings:Editon your specific zone — required only if you wantspirens doctorto verify the SSL/TLS mode (public deployments that proxy records).
A token scoped like this can read and modify DNS records in one zone, nothing else. It cannot create new zones, read your account billing, or touch other zones.
See 02 — DNS & Cloudflare: Scoped API token for the walkthrough.
Per-consumer tokens (optional)¶
The single token is reused by four consumers: Traefik (DNS-01), DDNS,
dns-sync, and spirens doctor / cleanup-acme-txt. If you're uncomfortable
reusing one token, generate separate tokens per consumer with only the
scopes each needs. All four components read their token from the same env
var by default, but you can wire each service to its own var in
compose/single-host/compose.*.yml.
Firewall: the Docker iptables trap¶
Docker manipulates iptables rules directly, bypassing ufw. A Docker
container with a published port (-p 1234:1234) is reachable from the
internet even when ufw status claims port 1234 is blocked.
This has bitten many self-hosters. SPIRENS doesn't publish any container
ports that shouldn't be public (Traefik :80/:443 and Kubo swarm
:4001), but if you add your own services, verify the published-ports
list and add DOCKER-USER chain rules if you need to restrict:
# Allow established connections
iptables -I DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow from your trusted subnet
iptables -I DOCKER-USER -s 10.0.0.0/8 -j ACCEPT
# Drop everything else to Docker-published ports
iptables -A DOCKER-USER -j DROP
See Docker's iptables guide for the full story.
Kubo's admin API¶
Kubo's /api/v0/* can:
- delete all your pinned content
- change your node's identity (loses peer reputation permanently)
- connect to arbitrary peers and consume your bandwidth
- stream every file you host through to anyone who asks
SPIRENS binds port 5001 to 127.0.0.1 only and exposes it to other
containers via the spirens_backend network. dweb-proxy is the only
thing that reaches it. Do not publish port 5001 on the host even on
a trusted LAN.
If you need to use the Kubo CLI from your workstation, SSH-tunnel instead of publishing the port:
ssh -L 5001:127.0.0.1:5001 your-host
# then, on your workstation:
ipfs --api /ip4/127.0.0.1/tcp/5001 id
Traefik dashboard exposure¶
The dashboard is a live view of every route, middleware, service, and their states. Defaults:
- Cloudflare orange-cloud can be enabled for origin hiding (optional).
- IP allowlist middleware (
dashboard-ipallow@file) — RFC1918 by default. Expand viaTRUSTED_CIDRSin.env. - Basic-auth (bcrypt, stored as a Docker secret).
All three should stay enabled even in dev. Losing (2) and (3) together means an attacker who guesses the subdomain has a full admin panel.
eRPC rate limits and abuse¶
eRPC ships with a default 500 req/s per-client limit. On a public
deployment, this is the main dial between "generous" and "someone drained
my Alchemy quota in an afternoon". Budget profiles in
config/erpc/erpc.yaml
have three tiers — tune them down for paid vendors you care about.
Consider also:
- Adding Traefik-level rate limiting on
rpc.example.comfor per-IP caps (see 04 — Deployment Profiles: Rate limiting). - Putting the RPC endpoint behind Cloudflare's bot-fight mode for public deployments. eRPC is HTTP-only JSON-RPC, so CF's proxy works cleanly.
IPFS gateway abuse¶
A public IPFS gateway serves whatever anyone asks for. Attackers use public gateways to:
- Rate-limit bypass — your gateway becomes their DoS vector.
- Illegal content laundering — hash-based routing means the content looks like it's "from" you.
- Bandwidth draining on metered hosts (see the cost callout in 08 — IPFS).
Mitigations: Cloudflare proxy the gateway (caches by URL, absorbs most abuse), Traefik rate limits, or IP-allowlist the gateway if you don't need public access.
TLS hygiene¶
- Always use DNS-01 for wildcards. Don't roll your own HTTP-01 on wildcard hosts — the ACME spec doesn't allow it.
- Verify what's actually served, not what the renewer logs say. A
stale file mount or orange-clouded CDN can silently mask a renewal
failure.
openssl s_clienttells the truth:
openssl s_client -connect rpc.example.com:443 -servername rpc.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
- Staging first. LE production rate limits are unforgiving: 50 certs
per registered domain per week and 5 failed validations per account
per hostname per hour. Iterate on
caServer=https://acme-staging-v02.api.letsencrypt.org/directoryuntil issuance is reliable.
Rotation schedule¶
A reasonable calendar for a long-running deployment:
| Asset | Rotate every | Trigger |
|---|---|---|
| Dashboard password | 6 months | Any operator change, or suspicion |
| Cloudflare API token | 12 months | Or when scope needs change |
REDIS_PASSWORD |
12 months | Or after a compromise |
| Vendor RPC keys | 12 months | Or at key renewal time |
| LE certs | Automatic | 30 days before expiry, via Traefik |
| CF Origin Cert (if used) | 15 years | Manual; set a calendar reminder |
What SPIRENS does not do¶
Things intentionally out of scope — so you can plug them in knowingly:
- SIEM / log forwarding. Container logs stay local. Loki or Promtail + Grafana are the common add-ons.
- Intrusion detection. No fail2ban equivalent in the default stack. Consider CrowdSec — it has a first-class Traefik bouncer.
- Backup. Named volumes are not backed up. At minimum,
letsencrypt/acme.json, your IPFS pin list (ipfs pin ls), and your.envshould be in a recoverable location outside the host. - MEV / validator security. If you enable the Ethereum node and run a validator, that's a separate security model entirely — see 06 — Ethereum Node and seek validator-specific guides.
Reporting¶
If you find a security issue in SPIRENS itself (not in upstream Traefik, Kubo, eRPC, or dweb-proxy — report those to their respective projects), open an issue at github.com/MysticRyuujin/spirens/issues or use GitHub's private vulnerability reporting.