Hardened I2P Eepsite with i2pd
Created September 12, 2026 • Last updated September 12, 2026 • 7 min read • 260 views
This guide hosts a hardened I2P eepsite with i2pd on a current Debian or Ubuntu server. i2pd is the C++ I2P router: one binary, two config files, a systemd service, and no Java. You get a router that does nothing except publish your site, with the web console, the proxies, and every client API switched off.
Why i2pd for a hardened I2P eepsite
The Java router from geti2p.net ships a router console and a bundle of web apps, from a torrent client to webmail. That is a desktop package. A server needs the opposite: a small daemon with a flat config you can read in one screen. i2pd is that daemon, and it lives in the distribution repositories. Never run both routers on one host.
Three things that differ from a plain client
I2P is peer to peer. Your router performs far better when other routers can reach it, so you open one inbound port for TCP and UDP. A firewalled router still works, but slower and with fewer peers.
Your router relays transit traffic for others by default. That costs bandwidth but gives you cover traffic and helps the network. Turn it off only when bandwidth is tight.
The network takes time. The first start reseeds and builds tunnels, so expect five to ten minutes before the site answers. The router keeps improving over the following hours. Nothing is broken during that window. Reseed errors in the log are normal as well: the router tries several reseed servers and moves on when one fails, which can add a few minutes.
Install i2pd
The package sits in the distribution repositories with only boost, OpenSSL, zlib, and a small UPnP library as dependencies. Install it from there. If you need a newer release than the distribution ships, PurpleI2P maintains an Ubuntu PPA at ppa:purplei2p/i2pd.
Minimal i2pd.conf
Replace the shipped /etc/i2pd/i2pd.conf with this. Every service a server does not need is set to false. Pick your own high port and keep it, since the firewall below must match. Comments use a hash, not a semicolon.
ipv4 = true
ipv6 = false
port = 12345
bandwidth = O
notransit = false
log = file
logfile = /var/log/i2pd/i2pd.log
loglevel = error
[http]
enabled = false
[httpproxy]
enabled = false
[socksproxy]
enabled = false
[sam]
enabled = false
[bob]
enabled = false
[i2cp]
enabled = false
[i2pcontrol]
enabled = false
[upnp]
enabled = false
[reseed]
verify = true
The [http] section is the web console, which you do not need on a server. The proxies, SAM, BOB, I2CP, and I2PControl are client interfaces. The bandwidth letter sets the share your router offers: O is 256 KBps, P is 2048, X is unlimited, or give a number in KBps. The reseed verify flag enforces signature checks on the bootstrap bundle. Keep loglevel at error. At warn, the first hour fills the log with profiling and session messages that look alarming but are normal while the router integrates. One error at the very first start, about a RouterInfo file it cannot open, is harmless too: the file does not exist yet.
The server tunnel
The site itself is one block in /etc/i2pd/tunnels.conf. It points the tunnel at a web server on loopback. The keys path is relative to /var/lib/i2pd/. When the file does not exist, i2pd creates it on start, and that key pair is your address.
[site]
type = http
host = 127.0.0.1
port = 8080
keys = site.dat
Important
The file /var/lib/i2pd/site.dat is the identity. Lose it and the address is gone, since there is no rotation and nothing to recover from. Back it up offline and never commit it to version control.
Nginx on loopback
Serve the site from loopback only, so nothing listens on the public interface. Keep the server block a catch-all and use relative URLs throughout the site, because the Host header that arrives through the tunnel is never a public name. Hide the version banner, disable directory listing and absolute redirects, and send errors to a page that names nothing. Turn the ETag off as well: it encodes file modification time and size, so a clearnet copy of the same files would carry identical ETags and link the two sites. Save this as /etc/nginx/sites-available/eepsite.
server {
listen 127.0.0.1:8080;
server_name _;
root /var/www/eepsite;
index index.html;
server_tokens off;
absolute_redirect off;
port_in_redirect off;
autoindex off;
access_log off;
etag off;
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
location ~ /\. {
deny all;
}
error_page 403 404 /error.html;
error_page 500 502 503 504 /error.html;
location = /error.html {
internal;
}
}
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
Firewall
Set a default drop on input and output. Inbound, allow return traffic, loopback, SSH, and the i2pd port on both TCP and UDP. Outbound, allow only the i2pd user to open new connections, plus NTP for the clock. The web server inherits the drop, so a compromised site cannot reach out and expose the host. Write this to /etc/nftables.conf and apply it.
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
tcp dport 22 accept
# i2pd transport port. Must match port in i2pd.conf.
tcp dport 12345 accept
udp dport 12345 accept
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy drop;
ct state established,related accept
oif "lo" accept
# Only the i2pd daemon may open new outbound connections.
meta skuid "i2pd" accept
udp dport 123 accept
}
}
Warning
The output drop also blocks the package tool over clearnet. Apply the firewall only after everything is installed, and open the rule when you need to update, or route updates through a proxy you control.
Read the b32 address
With the console off, read the address from the key file with keyinfo from i2pd-tools. Build the tools once. The same build gives you vain for a vanity I2P address. Clone with submodules, since the repository pulls in the i2pd library.
c22brvyamaj2wxuz5z5zmxt4kwo7wh6yd3pzxigm5bpwfp6jbuza.b32.i2p
Explain command
| --recursive | Fetches the git submodules, which the build needs for the i2pd library. |
If you would rather not compile, set enabled = true and address = 127.0.0.1 under [http], restart i2pd, read the address from the I2P tunnels page on port 7070 over an SSH tunnel, then switch the console off again.
Test from a client
Use a separate machine with i2pd where the HTTP proxy is enabled on its default port 4444. Give the router a few minutes after first start, then fetch the site through the proxy. Replace $I2P_ADDRESS with the b32 hostname from keyinfo. Note that no Server header comes back: the i2pd http tunnel strips it, so the web server never announces its name through the tunnel.
HTTP/1.1 200 OK Content-Type: text/html Content-Length: 11 Connection: close Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none' Referrer-Policy: no-referrer X-Content-Type-Options: nosniff X-Frame-Options: DENY Accept-Ranges: bytes Proxy-Connection: close
Explain command
| -x http://127.0.0.1:4444 | Sends the request through the local i2pd HTTP proxy. |
On the server, confirm nothing web related listens on a public address. Only SSH and the i2pd transport port should appear on a routable interface.
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
tcp LISTEN 0 511 127.0.0.1:8080 0.0.0.0:* users:(("nginx",pid=901,fd=6))
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=744,fd=3))
tcp LISTEN 0 1024 0.0.0.0:12345 0.0.0.0:* users:(("i2pd",pid=1130,fd=12))
udp UNCONN 0 0 0.0.0.0:12345 0.0.0.0:* users:(("i2pd",pid=1130,fd=13))
Operations
Back up site.dat offline and test a restore on a throwaway host. To check reachability, enable the console on loopback as described above and read Network status on its front page. Firewalled means the inbound port is not reachable, which points at the firewall or the provider. Raise the bandwidth letter once you see how much transit the host carries, or set notransit to true if the traffic is a problem.
To serve more than one site, add a second block to tunnels.conf with its own keys file and its own loopback port. Each block is a separate address on the same router.
The generated address is random. To pick the first characters yourself, grind a vanity I2P address with vain and drop it in as site.dat.
References
-
i2pd configurationDOCS: i2pd configuration, docs.i2pd.website/en/latest/user-guide/configuration (opens in new tab)
Every i2pd.conf option with defaults.
-
Server, http, and client tunnel options for tunnels.conf.
-
keyinfo, vain, and other utilities for keys and addresses.
-
The C++ I2P router used in this guide.