Subscribe and receive upto $1000 discount on checkout. Learn more
Subscribe and receive upto $1000 discount on checkout. Learn more
Subscribe and receive upto $1000 discount on checkout. Learn more
Subscribe and receive upto $1000 discount on checkout. Learn more
Configuring Linux Servers to Work Safely Behind Firewalls

When “it’s behind the firewall” stops being a security plan

In the early days, a Linux server behind an enterprise firewall feels like a safe bet. The network team has rules. The perimeter is monitored. The server is “internal.” Then the environment grows. A second subnet appears. A third-party integration needs outbound access. A monitoring agent is added. A jump host becomes a shared dependency. Suddenly, the firewall is no longer a single protective wall—it’s a set of moving parts, exceptions, and assumptions.

This is where incidents quietly begin: a service binds to all interfaces, a default route changes, DNS starts leaking to the wrong resolver, or a “temporary” rule becomes permanent. The fix is rarely dramatic. It’s disciplined. We make the server predictable, minimize what it can do, minimize what can reach it, and verify every change. That is the principle of least privilege applied to Linux in a real network.

Prerequisites and assumptions

Before we touch commands, we need to be explicit about what we are configuring and what we are not. These assumptions keep the steps safe and production-grade.

  • Platform: Linux server running systemd. The commands below are written for Debian/Ubuntu-style systems (APT, UFW). If we are on RHEL-family, the same concepts apply but the package manager and firewall tooling differ.
  • Access method: We have console access (hypervisor console, iDRAC/iLO, cloud serial console) or an existing, working SSH path through the enterprise firewall. We do not rely on making risky network changes without a recovery path.
  • Privileges: We have a non-root admin account with sudo. We will avoid staying logged in as root.
  • Network reality: The enterprise firewall controls north-south traffic. We still configure host controls because internal lateral movement is a real threat and because firewall rules drift over time.
  • Change control: We schedule a maintenance window if this is a shared system. We keep an active session open while applying firewall changes, and we validate after each step.
  • Least privilege: We will only allow what is required for operations: administration (SSH), time sync, DNS, package updates, logging/monitoring, and the specific application ports that are truly needed. We will not rely on broad allowances.

Step 1: Capture the server’s current network identity

Before we harden anything, we need to know what the server thinks its interfaces, routes, and DNS settings are. This prevents us from writing rules against the wrong interface or troubleshooting the wrong resolver later.

set -euo pipefail

hostnamectl
ip -br link
ip -br addr
ip route
resolvectl status || true

We have now captured the baseline: hostname, interfaces, IP addresses, routing, and DNS resolver state. If anything looks unexpected (wrong default route, multiple active interfaces, or DNS pointing somewhere unapproved), we pause and fix that first—hardening on top of a broken baseline creates fragile systems.

Step 2: Update packages safely and enable unattended security updates

Behind enterprise firewalls, patching often fails quietly due to proxy requirements or restricted egress. We will first confirm we can update, then enable unattended security updates so the server does not drift into known-vulnerable territory.

sudo apt-get update
sudo apt-get -y upgrade

The system has now pulled the latest package metadata and applied available upgrades. If this step fails, it usually indicates DNS, proxy, or egress restrictions that we should address before continuing.

Next, we enable unattended security updates. This is not about “auto-upgrading everything”; it is about consistently applying security fixes with minimal operational overhead.

sudo apt-get -y install unattended-upgrades apt-listchanges
sudo dpkg-reconfigure -plow unattended-upgrades

Unattended upgrades are now installed and configured via Debian’s standard mechanism. We should still manage major version upgrades through change control, but security updates will no longer depend on someone remembering to run them.

We verify the service timers are present and active.

systemctl list-timers --all | grep -E 'apt|unattended' || true
systemctl status unattended-upgrades --no-pager || true

We have now confirmed the scheduling and current status. If the service is inactive, we investigate logs later in the troubleshooting section.

Step 3: Create a controlled admin path with SSH hardening

In enterprise environments, SSH is usually the operational lifeline. The goal is not to make SSH “clever”; it is to make it boring, predictable, and resistant to common abuse. We will enforce key-based access, disable direct root login, and reduce attack surface.

First, we confirm the SSH service state and which address/port it is listening on. We do this before changing anything so we can compare after.

systemctl status ssh --no-pager
ss -tulpn | grep -E '(:22)s' || true

We have now confirmed whether SSH is running and listening. If SSH is not listening where expected, we stop and resolve that before applying firewall rules.

Next, we create a dedicated admin group and ensure our current sudo-capable account is in it. This gives us a clean way to restrict SSH access to a known set of operators.

sudo groupadd -f sshadmins
sudo usermod -aG sshadmins "$USER"
id

The server now has an sshadmins group, and our current user is a member. The id output should show sshadmins listed.

Now we harden the SSH daemon configuration. We will keep it explicit and minimal: no root login, no password authentication, and only members of sshadmins can log in via SSH.

sudo cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F)

sudo tee /etc/ssh/sshd_config >/dev/null <<'EOF'
# Managed configuration: secure baseline for enterprise environments
# Principle of least privilege: only approved admins, key-based auth, no root login.

Port 22
Protocol 2

# Listen on all interfaces by default; firewall and network segmentation control reachability.
# If we must bind to a specific management interface, we do it intentionally after validation.
ListenAddress 0.0.0.0
ListenAddress ::

PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
UsePAM yes

PubkeyAuthentication yes
AuthenticationMethods publickey

AllowGroups sshadmins

X11Forwarding no
AllowTcpForwarding no
PermitTunnel no
GatewayPorts no

ClientAliveInterval 300
ClientAliveCountMax 2

LogLevel VERBOSE

Subsystem sftp /usr/lib/openssh/sftp-server
EOF

sudo sshd -t
sudo systemctl restart ssh

We have now replaced the SSH configuration with a controlled baseline, validated syntax with sshd -t, and restarted the service. The key change is that only key-based logins are allowed, root cannot log in directly, and only members of sshadmins can access SSH.

We verify SSH is still listening and that the service is healthy.

systemctl status ssh --no-pager
ss -tulpn | grep -E '(:22)s' || true

If SSH is not listening or the service fails, we immediately revert using the backup file and restart SSH. We cover that in troubleshooting.

Step 4: Turn on host firewalling with least privilege using UFW

The enterprise firewall is necessary, but it is not sufficient. Host firewalling gives us a second control plane that travels with the server, survives network changes, and limits lateral movement. We will implement a default-deny stance and then allow only what operations require.

First, we install UFW and confirm it is available.

sudo apt-get -y install ufw
sudo ufw --version

UFW is now installed. Next, we identify the primary interface so we can reason about where traffic enters and exits.

EXT_IFACE=$(ip route show default 0.0.0.0/0 | awk '{print $5; exit}')
echo "$EXT_IFACE"

We have now captured the default-route interface into EXT_IFACE. This helps us validate that firewall behavior aligns with the server’s actual routing.

Now we set a conservative baseline: deny inbound by default, allow outbound by default, and log at a low level. This aligns with least privilege while keeping operations workable in most enterprise networks where outbound is controlled upstream.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw logging low

The firewall policy is now set. Inbound connections will be blocked unless explicitly allowed. Outbound is allowed at the host level, but enterprise egress controls still apply.

Next, we allow SSH. We do this before enabling the firewall to avoid locking ourselves out.

sudo ufw allow 22/tcp comment 'SSH administration'

SSH is now explicitly allowed through the host firewall.

Now we enable UFW and confirm status. Enabling applies the rules immediately and persists across reboots.

sudo ufw --force enable
sudo ufw status verbose

The host firewall is now active, persistent, and enforcing a default-deny inbound stance. The status output should show SSH allowed and default policies as configured.

Step 5: Reduce exposed services and confirm listening sockets

Firewalls are not a substitute for not running unnecessary services. The cleanest security win is to stop listening on ports we do not need. We will inventory listening sockets and disable what is not required.

sudo ss -tulpn

This output is our truth: it shows what is actually reachable on the network. If we see services we do not recognize (for example, a database listening on all interfaces), we either bind it to localhost, restrict it to an internal subnet, or disable it entirely.

We also confirm which services are enabled at boot, because persistence matters in production.

systemctl list-unit-files --type=service --state=enabled

We now have a list of services that will start automatically. This is where “temporary” agents often become permanent. We keep only what we need.

Step 6: Apply kernel and network hardening with sysctl

Behind firewalls, the most common network attacks are not exotic—they are lateral movement, spoofing attempts, and opportunistic scanning. Sysctl hardening helps the kernel behave defensively: it reduces information leakage, blocks certain redirect behaviors, and improves baseline resilience.

We will create a dedicated sysctl file so our changes are explicit, versionable, and persistent across reboots.

sudo tee /etc/sysctl.d/99-niilaa-hardening.conf >/dev/null <<'EOF'
# NIILAA baseline hardening - principle of least privilege
# Network protections
net.ipv4.ip_forward = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1

# Reduce kernel info exposure
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.unprivileged_bpf_disabled = 1

# File system protections
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
EOF

sudo sysctl --system

We have now applied hardened kernel parameters and made them persistent via /etc/sysctl.d. The sysctl --system output shows which values were loaded.

We verify a few key values to ensure the system is actually enforcing them.

sysctl net.ipv4.ip_forward
sysctl net.ipv4.conf.all.accept_redirects
sysctl kernel.kptr_restrict
sysctl kernel.dmesg_restrict

These checks confirm that forwarding is off (unless we intentionally run a router), redirects are not accepted, and kernel information exposure is reduced.

Step 7: Make outbound dependencies explicit: DNS, time, and package egress

Most “behind the firewall” outages are outbound problems: DNS can’t resolve, NTP can’t sync, or package repositories are blocked. We do not solve this by loosening inbound rules. We solve it by making dependencies explicit and verifying them.

DNS verification

We confirm which resolver is in use and whether name resolution works. This matters because security controls often depend on DNS (updates, logging endpoints, internal service discovery).

resolvectl status || true
getent hosts deb.debian.org || true
getent hosts security.ubuntu.com || true

If name resolution fails, we typically have either a blocked path to internal DNS, an incorrect resolver configuration, or a split-DNS expectation that is not met on this subnet.

Time synchronization verification

Time drift breaks TLS, authentication, and log correlation. We verify that systemd-timesyncd (or another NTP client) is active and synchronized.

timedatectl
timedatectl timesync-status || true
systemctl status systemd-timesyncd --no-pager || true

If the clock is not synchronized, we work with the network team to ensure NTP is reachable to the approved time sources. We do not “fix” time by disabling validation or ignoring TLS errors.

Outbound connectivity verification

We validate that the server can reach required endpoints through the enterprise firewall. We keep this simple and observable.

ip route
curl -I https://example.com --max-time 10 || true

This confirms routing and basic HTTPS egress. If this fails, we investigate proxy requirements or egress restrictions rather than changing inbound exposure.

Step 8: Logging and auditability that survives real operations

Security without visibility is guesswork. We want logs that help us answer: who logged in, what changed, and what was blocked. We will ensure journald persistence and enable basic auditing.

First, we make journald persistent so logs survive reboots. This is critical when diagnosing incidents that involve restarts.

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo sed -i 's/^#?Storage=.*/Storage=persistent/' /etc/systemd/journald.conf
sudo systemctl restart systemd-journald

Journald is now configured to store logs persistently under /var/log/journal. After restart, logs will survive reboots and provide continuity during investigations.

We verify journald is using persistent storage.

journalctl --disk-usage
grep -E '^s*Storage=' /etc/systemd/journald.conf

This confirms both configuration and actual disk usage for logs.

Next, we enable auditd for basic security auditing. This is a common enterprise expectation and helps with accountability.

sudo apt-get -y install auditd audispd-plugins
sudo systemctl enable --now auditd
sudo systemctl status auditd --no-pager

Auditd is now installed, enabled at boot, and running. We can later add organization-specific audit rules, but even the baseline improves traceability.

Step 9: Reboot-safe verification checklist

Production hardening is not complete until we prove it survives a reboot and still behaves as expected. We will validate the key controls: SSH, firewall, sysctl, time sync, and listening services.

We reboot during a controlled window and keep console access available.

sudo systemctl reboot

The server will restart. After reconnecting, we verify the state again.

set -euo pipefail

systemctl status ssh --no-pager
sudo ufw status verbose
sysctl net.ipv4.ip_forward
timedatectl
sudo ss -tulpn

If these checks pass, we have a stable baseline: controlled admin access, default-deny inbound firewalling, hardened kernel parameters, synchronized time, and a clear view of what is listening.

Troubleshooting

SSH access suddenly fails after hardening

  • Symptom: SSH connection is refused or times out.
  • Likely causes: UFW enabled without allowing SSH; SSH service failed to restart due to config error; we are not in the sshadmins group; key-based auth not set up.
  • Fix: Use console access, then validate and revert safely.
sudo ufw status verbose
sudo ufw allow 22/tcp comment 'SSH administration'
sudo systemctl restart ssh
sudo systemctl status ssh --no-pager

# If sshd config is broken, revert to backup and restart
ls -1 /etc/ssh/sshd_config.bak.* | tail -n 1
sudo cp -a "$(ls -1 /etc/ssh/sshd_config.bak.* | tail -n 1)" /etc/ssh/sshd_config
sudo sshd -t
sudo systemctl restart ssh

We have now confirmed firewall allowance, restored a known-good SSH configuration if needed, and restarted SSH with syntax validation.

Package updates fail behind the firewall

  • Symptom: apt-get update fails with timeouts or “Temporary failure resolving”.
  • Likely causes: DNS not reachable; proxy required; egress blocked to repositories.
  • Fix: Confirm DNS and route first, then align with enterprise proxy/egress policy.
ip route
resolvectl status || true
getent hosts deb.debian.org || true

# If a proxy is required, we configure it explicitly (example file creation without embedding secrets)
sudo tee /etc/apt/apt.conf.d/90proxy >/dev/null <<'EOF'
# Acquire::http::Proxy "http://proxy.example:3128";
# Acquire::https::Proxy "http://proxy.example:3128";
EOF

We have now validated routing and DNS, and we have a safe place to configure proxy settings in a controlled way that aligns with enterprise standards.

Time is not synchronized

  • Symptom: TLS errors, authentication failures, or timedatectl shows “System clock synchronized: no”.
  • Likely causes: NTP blocked; wrong time sources; timesync service disabled.
  • Fix: Ensure the service is enabled and confirm reachability to approved NTP sources.
systemctl enable --now systemd-timesyncd
systemctl status systemd-timesyncd --no-pager
timedatectl timesync-status || true

We have now ensured the time sync service is enabled and checked its status. If it still cannot sync, the remaining issue is almost always network policy, not the server.

Common mistakes

Mistake: Enabling the firewall before allowing SSH

  • Symptom: Existing SSH session drops; new SSH connections time out.
  • Fix: Use console access, allow SSH, then re-enable UFW.
sudo ufw allow 22/tcp comment 'SSH administration'
sudo ufw --force enable
sudo ufw status verbose

We have restored SSH reachability through the host firewall and confirmed the active rules.

Mistake: Locking out admins by restricting SSH groups

  • Symptom: “Permission denied (publickey)” for valid keys, or login works for some accounts but not others.
  • Fix: Ensure the account is in sshadmins, then reconnect.
id
sudo usermod -aG sshadmins "$USER"
getent group sshadmins

We have confirmed group membership and ensured our account is authorized for SSH access under the least-privilege model.

Mistake: Assuming “behind the firewall” means services can bind anywhere

  • Symptom: Unexpected listening ports appear in scans or internal monitoring; lateral movement risk increases.
  • Fix: Inventory listening sockets and restrict services to localhost or required subnets, then enforce with UFW rules.
sudo ss -tulpn
sudo ufw status verbose

We have a clear view of what is listening and what the host firewall allows. From here, we tighten service bind addresses and firewall allowances to match actual requirements.

How do we at NIILAA look at this

This setup is not impressive because it is complex. It is impressive because it is controlled. Every component is intentional. Every configuration has a reason. This is how infrastructure should scale — quietly, predictably, and without drama.

At NIILAA, we help organizations design, deploy, secure, and maintain Linux server environments behind enterprise firewalls with clear standards: least privilege access, repeatable baselines, auditable change, and verification that holds up under real operations. We align host controls with network policy, reduce lateral movement risk, and keep systems maintainable as teams and environments grow.

Website: https://www.niilaa.com
Email: [email protected]
LinkedIn: https://www.linkedin.com/company/niilaa
Facebook: https://www.facebook.com/niilaa.llc

Leave A Comment

All fields marked with an asterisk (*) are required