~/blog/services/nexterm-real-client-ip.md
Nexterm logged my reverse proxy instead of the client
// tl;dr
Nexterm logged my reverse proxy as the source of every session because Express wasn't trusting the proxy. I patched it with a bind mounted index.js. Current releases read TRUST_PROXY, so drop the patch and set that to the proxy's own address, not true.
I put Nexterm behind my reverse proxy and later went through its audit log. Every session came from the same address. It was the proxy.
That's not much of an audit log for a tool whose whole job is opening shells on other machines.
What the log was recording
I was running Nexterm from the all in one image, nexterm/aio:latest. The web UI listens on 6989 and the engine's control port is 7800. The web port was published only on the host's overlay address, and everyone went through the proxy at nexterm.example.lab:
services:
nexterm:
image: nexterm/aio:latest
ports:
- "100.64.x.x:6989:6989"
volumes:
- nexterm_data:/app/data
The server is a Node and Express app under /app/server. Express takes req.ip from the TCP connection unless it's told to trust a proxy. The build I had never told it. So every login came from the last hop, and the last hop is always the proxy.
The proxy was already sending X-Forwarded-For. The fix below proves that. Nothing in that build read it.
No setting to flip, at the time
I went looking for an environment variable first. The image I pulled didn't have one. No TRUST_PROXY, nothing in the compose examples, and no app.set("trust proxy", ...) anywhere in the server code. I hadn't pinned a version, so I can't tell you which release that was.
Check your version first
Current Nexterm has the setting. v1.2.2-BETA, released in July 2026, reads a TRUST_PROXY environment variable and hands it to Express. It takes true, a hop count, or a list of addresses, and it logs a warning if you use true. On that release or newer, skip my patch and set the variable:
environment:
TRUST_PROXY: "<proxy address>"
The rest of this is what I did before that existed, and why it's worth undoing.
One line, bind mounted
The fix was one line in /app/server/index.js, right after the app is created:
const app = expressWs(express()).app;
app.set("trust proxy", true);
I didn't want to build my own image for one line. So I copied the image's index.js out to the host, added the line, and mounted it back over the original, read only.
Copy the file out of a running container:
docker cp <container>:/app/server/index.js /srv/nexterm/index.js
Then mount it back in:
volumes:
- nexterm_data:/app/data
- /srv/nexterm/index.js:/app/server/index.js:ro
After a recreate, new sessions in the audit log showed real client addresses.
One side note from the same container. The database is at /app/data/nexterm.db, but the storage path is relative to /app. Anything you run against it by hand needs that working directory:
docker exec -w /app <container> node <script>
What the shortcut costs
That mounted file is the whole upstream index.js plus one line. When the image updates, the copy doesn't. The container ends up running an old entry point against new code.
Sometimes that would break loudly. The quiet case is worse. Pull a release that has TRUST_PROXY, keep the old mount, and the older file replaces the new handling. Set the variable and nothing happens.
So as long as a mount like that is in place, every update means pulling the new image, diffing its index.js against yours, and carrying the line forward before you recreate:
docker run --rm --entrypoint cat nexterm/aio:latest /app/server/index.js > /tmp/index.upstream.js
diff /tmp/index.upstream.js /srv/nexterm/index.js
The diff should show one added line. Anything more and you're behind. If upstream already has TRUST_PROXY, drop the mount.
What the log showed after
Real client addresses, with one catch. A public or home address only showed up when the client came in over the internet through the proxy. Devices on the overlay showed their 100.64.x.x address instead. Both are right. They're different paths in.
What I checked and what I didn't
- seen: every audit entry showed the proxy before the patch
- seen: no
trust proxycall anywhere in that build's server code - seen: real client addresses on new sessions after the recreate
- not checked: which Nexterm release I was on, or which release first added
TRUST_PROXY
What I'd tell myself
trust proxy set to true believes whatever X-Forwarded-For says. That only works if nothing can reach the app port except the proxy. Binding the port to one network keeps it off the others. It doesn't keep it away from anything else on that same network, and any machine there could send a fake header straight to the port.
A hop count of 1 doesn't help there. The README suggests 1 for a single Nginx or Traefik, which is fine when the proxy is the only way in. Express's docs warn that if a client can reach the app over a shorter path than the hop count assumes, it can supply any value it likes. A machine hitting the app port directly is that shorter path.
Trusting the proxy's own address holds up. Express only reads the header when the connection actually comes from the proxy, and everyone else gets logged as themselves. That's the one to set, through TRUST_PROXY this time instead of a patched file.
$ ./ask --help
Stuck on something similar? Ask below and I'll try to help you work through it. It helps to include:
- --running hardware, OS, versions
- --tried what you've already tried
- --error the exact message, word for word
Comments are public, so leave out passwords, keys and public IP addresses.