Table of Contents
- i2pforge Operator Runbook
i2pforge Operator Runbook
A correctly-built container deployed incorrectly is not secure. This is the field manual for running i2pforge against a state-level threat model.
1. Deploy
Use a generated config (i2pforge setup, the web wizard, the AppImage, or
docker run -it i2pforge setup -o /out) or the shipped docker-compose.yml.
Do not weaken the security flags. They are the product:
cap_drop: [ALL]+ onlyCHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGIDsecurity_opt: [no-new-privileges:true],read_only: truepids_limit: 512,mem_limit: 1g,cpus: 2- tmpfs for
/tmp,/etc/i2pd,/var/lib/i2pd; the keys bind mount only - the backend on an
internal: truenetwork with noports:mapping logging: { driver: none }
Verify a running container at any time: i2pforge doctor and
docker-bench-security -c container_runtime (expect 0 warnings).
1a. Which mode?
- Gateway (
--mode eepsite|tcp --backend host:port): you run the backend; i2pforge gives it an address. See §5 (don't let the backend leak its identity) and §4 (backend IP is pinned at startup). - Direct hosting (
--mode direct): a bundled read-only nginx serves./site. Drop your files in./site(deploy.sh makes them world-readable so the unprivileged nginx can read them) and re-run./deploy.sh. The address is unchanged across content updates. - Mirror (
--mode mirror --origin https://… [--mirror-mode full|lite]): mirrors a clearnet site. The companion needs outbound internet to reach the origin (it is on thei2p-externalnetwork for that reason); the visitor does not. Prefer full (anti-leak) unless you own the origin and its links are relative. If the origin's IP/TLS changes,docker compose restart mirror. Mirroring a site you do not control is the weakest posture. Full mode's CSP blocks third-party clearnet assets, which can render some pages incompletely. Read the mirror leak model in SECURITY.md.
2. The keys ARE your address, back them up
./i2p-keys/<service>.dat (or .dat.i2pfk1) is the only thing that proves
ownership of your .b32.i2p. Lose it and the address is gone forever; leak it
and someone can impersonate your eepsite.
i2pforge backup --out keys.tar.age # age, gpg, or openssl (in that order)
i2pforge restore --input keys.tar.age # on a new host
Store the backup offline and separately from any encrypt-at-rest passphrase. Two backups of the same keys dir are byte-identical (deterministic tar) so you can checksum them.
3. Encrypt at rest (recommended)
Wrap the on-disk key so a stolen ./i2p-keys/ is useless without the passphrase
(scrypt N=2¹⁷ + ChaCha20-Poly1305; see SECURITY.md).
i2pforge encrypt-key --delete-plaintext
# then mount the passphrase on every start (compose secrets / -v):
# I2P_KEY_PASSPHRASE_FILE -> a file containing the passphrase, mode 0400
The container decrypts into in-memory tmpfs only; plaintext never touches the persistent disk. Migration is atomic and crash-safe (round-trip-verified before the plaintext is securely wiped). If you lose the passphrase the eepsite is unrecoverable. There is no recovery flow.
4. ⚠️ Backend IP is pinned at startup (operational gotcha)
i2pforge resolves the backend hostname to an IP once, at container start (so a later DNS change can't silently repoint your tunnel, a security feature). The consequence: if you redeploy/recreate the backend container and Docker gives it a new IP, the eepsite serves nothing until you also restart i2pforge.
# After any backend redeploy:
docker compose restart i2p
If your orchestration recreates the backend often, pin its IP (static
ipv4_address on the internal network) so the resolved IP never changes.
5. Don't let the backend leak its identity
i2pforge's http server tunnel rewrites the Host header to your .b32.i2p,
but response headers from your backend pass through unchanged. A
Server: nginx/1.27.0 or a default error page can fingerprint your host. Harden
the backend you point at:
- nginx:
server_tokens off;and strip/replace theServerheader. - Remove
X-Powered-By, framework version banners, and verbose error pages. - Don't log client info you don't need (I2P clients are not your users' IPs, but app-level analytics, beacons, or third-party fonts/CDNs reintroduce clearnet leaks, so serve everything from the eepsite).
6. Clock
I2P requires an accurate clock to build tunnels and validate routerinfos. The container inherits the host clock, so keep the host synced (NTP/chrony). A host clock skewed by more than a minute or two will prevent the router from participating in the network.
7. Rotate (destroy and regenerate identity)
i2pforge rotate # asks twice; archives the old keys, generates a fresh
# identity on next boot. The .b32.i2p CHANGES PERMANENTLY.
8. Incident response: suspected key compromise
If you believe ./i2p-keys/ (or the decrypted key, or the passphrase) was
exposed:
- Rotate immediately (
i2pforge rotate). The old address can be impersonated until you do. There is no revocation in I2P; rotation is the only remedy. - Publish the new
.b32.i2pthrough your out-of-band channels. - Treat the old address as burned. Investigate how the keys dir became readable (bind-mount permissions, host compromise, backup leak).
- If encrypt-at-rest was on and only the envelope (not the passphrase) leaked, the key is still protected, but rotate anyway if the passphrase strength is in doubt.
9. Updates
The image is pinned and reproducible; updates ship as new signed images, not in-place mutation. Pull a new release and recreate:
docker compose pull i2p && docker compose up -d i2p # keys persist on disk
Verify the signature first if you're under a real adversarial model
(scripts/verify-image.sh). Your .b32.i2p survives image updates. It lives
in the keys bind mount, not the image.