As part of building my self-hosted Hugo publishing workflow, I ended up with a surprisingly useful reminder:
Even if your deployment looks simple, it often relies on more than one network flow.
This came up while tightening firewall rules around my DMZ web server used for Hugo deployments.
The Setup (High Level)
My environment looks roughly like this:
- A trusted workstation where I write content
- An internal Git server (Gitea) on the LAN
- A public-facing web server in a DMZ
- A deployment script that pulls updates and rebuilds the Hugo site
The publishing workflow is:
- Edit content locally
- Commit + push to Git
- Trigger a deploy command remotely
- The web server pulls the latest content and rebuilds the site
Network Flow Overview
The deployment pipeline relies on two separate SSH connections.
Workstation → DMZ Web Server
- Used to trigger the deployment script.
DMZ Web Server → Internal Git Server
- Used by the deployment script to fetch the latest content.
Only these two SSH paths are required for the publishing workflow.## Network Flow Overview
Figure: SSH flows required for the Hugo deployment pipeline.
The Problem: SSH Deployment Broke After Firewall Lockdown
Once everything was running smoothly, I decided to lock down SSH access to the DMZ host.
So I created a firewall rule allowing:
Workstation → DMZ Web Server on port 22
That seemed correct.
But suddenly my deploy script (hillpush) failed.
As a test, I temporarily opened all ports — and everything worked again.
That told me immediately:
Another network dependency was being blocked.
The Key Realisation: Deployment Requires Two SSH Flows
Even though I was only SSH’ing into the web server, the web server itself also needed outbound access.
There are actually two separate SSH connections involved.
1. Workstation → Web Server (Remote Trigger)
This is the obvious one:
| |
Firewall requirement:
Allow SSH from the trusted workstation to the DMZ host.
2. Web Server → Git Server (Pull Latest Content)
Inside the deploy script, the DMZ server runs something like:
| |
That means the DMZ host must also be able to reach the Git server over SSH.
Firewall requirement:
Allow SSH from the DMZ host to the internal Git server.
The Fix: Minimal Rules, Maximum Security
Once I understood the full workflow, the firewall rules became very clean.
Rule A — Trusted Workstation → DMZ Web Server
Allow port 22 only from a trusted source.
Example:
Source: Trusted clients network
Destination: Web server
Port: 22
Rule B — DMZ Web Server → Internal Git Server
Allow outbound SSH only for Git pulls.
Example:
Source: Web server
Destination: Git server
Port: 22
Quick Connectivity Test
A useful test from the DMZ host is:
| |
If that fails, your deploy script will not be able to fetch updates.
Final Result
With both rules in place, I ended up with:
- SSH access locked down to a single trusted machine
- No unnecessary lateral movement between VLANs
- A working automated deployment pipeline
- Only the minimum required ports open
Takeaway
When hardening infrastructure, always map the full flow:
- Who connects to the server?
- What does the server need to connect to?
In my case:
Deployment wasn’t just “SSH in” — it was also “Git pull out”.
If you’re interested in the publishing pipeline itself, see how I implemented a Git-backed comment system using a private Gitea backend.
Comments