support@technetguy.site Remote IT Services · Worldwide · Get a Quote →
Security & Hardening

Protecting the Perimeter

SSL, firewalls, access control — and how a real production box gets hardened.

Digital padlock representing encryption and access control

Photo · Wikimedia Commons

The CIA Triad

Information security is built on three pillars: Confidentiality, Integrity, and Availability. On this server, that means encrypted tunnels (HTTPS), restrictive firewall rules, and monitoring that tells us the site is actually up — not just assuming it is.

Layers of Defense

SSL/TLS Encryption

Using Let's Encrypt to ensure that sensitive data remains encrypted while in transit between the client and the Nginx server.

SSH Hardening

Disabling password authentication in favor of Public Key Infrastructure (PKI) to stop brute-force bots in their tracks.

Rate Limiting

Public-facing endpoints like form submissions are throttled at the Nginx layer to blunt scripted abuse before it reaches the app.

fail2ban

Watches auth and web server logs, and temporarily bans IPs that show repeated failed login or probing behavior.

Real Hardening Configs

Textbook, standard-practice configuration — safe to publish and safe to run. Test every SSH change in a second session before closing your first one.

Key-only authentication, root login locked to keys only (not disabled entirely, so recovery via console still works), and a short grace period so half-open connections don't pile up.

sshd_config
# /etc/ssh/sshd_config.d/99-hardening.conf
PasswordAuthentication no
PermitRootLogin prohibit-password
PubkeyAuthentication yes
KbdInteractiveAuthentication no
X11Forwarding no
MaxAuthTries 3
LoginGraceTime 20
AllowTcpForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
bash
# Always validate before reloading — a typo here can lock you out
sshd -t && systemctl reload ssh

A shared snippet included in every server block, so every site on the box gets the same baseline browser-side protections for free.

nginx
# /etc/nginx/snippets/security-headers.conf
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

# included inside each server{} block with:
#   include snippets/security-headers.conf;

The full annotated version lives on the Linux hub — this is the SSH-focused core of it.

ini
# /etc/fail2ban/jail.local
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled  = true
port     = ssh
filter   = sshd
maxretry = 4
bantime  = 24h

Installing fail2ban and never enabling the service is a more common gap than people expect — systemctl is-active fail2ban is a five-second sanity check worth running after any provisioning script.

Suricata & IDS on OPNsense

A firewall decides what traffic is allowed through. An intrusion detection system reads the traffic that was allowed through and decides whether it looks malicious — two different jobs, both needed. Here's the real breakdown, from a production deployment running this exact stack.

A passive tap (a SPAN/mirror port feeding an IDS) only ever sees traffic — it can log an attack and alert on it, but it can't stop the first malicious packet from reaching its target. Running Suricata inline on the WAN interface means every packet passes through the inspection engine before it's forwarded, so a rule match can drop the packet instead of just logging it after the fact. The trade-off is real: inline inspection adds a small amount of latency and a bad rule can block legitimate traffic, which is exactly why rule tuning (next tab) matters.
On a real deployment, Suricata runs 7,700+ live rules pulled from maintained ruleset feeds — each one a signature or behavioral pattern matched against live traffic. "Tuning" doesn't mean writing thousands of custom rules; in practice it means confirming the rulesets you think are protecting you actually are. A real finding from this exact deployment: the ruleset categories had been downloaded and installed, but several were never actually toggled on in the running policy — installed is not the same as enabled, and that gap is invisible unless you explicitly check which categories are active, not just which ones are present on disk.

A single custom rule looks like this — alert-only here, but the same syntax with drop instead of alert is what inline blocking actually uses:

suricata rule
# /usr/local/etc/suricata/rules/local.rules
alert tcp any any -> $HOME_NET 22 (msg:"Repeated SSH auth attempts"; flow:to_server; \
  flags:S; threshold:type threshold, track by_src, count 10, seconds 60; \
  classtype:attempted-recon; sid:1000001; rev:1;)

And the interface Suricata actually listens on has to be explicitly bound — on OPNsense that's the WAN interface, set to inline (IPS) mode rather than the default passive (IDS) mode:

suricata.yaml
# /usr/local/etc/suricata/suricata.yaml
af-packet:
  - interface: igb0    # WAN — inline, not a mirrored/SPAN copy
    threads: auto
    cluster-type: cluster_flow
    defrag: yes
    use-mmap: yes
    ips: yes            # inline blocking mode, not alert-only

This is one piece of a larger open-source perimeter build — firewall, VPN, and IDS together. Full architecture and diagram in the Open-Source Enterprise Networking case study.

Case Study: Hardening This Very Server

Before this design system went live, the box hosting it went through a real hardening pass — the same investigate-before-you-fix process used on every client engagement. Five findings came out of it, closed one at a time:

A leaked-secrets exposure — a file that shouldn't have been publicly reachable was. Before rotating anything, the access logs were checked to see whether it had actually been pulled by anyone outside normal crawler traffic, and the contents were read to confirm whether real secrets were involved at all.
An internet-facing admin port with no reason to be open — a management interface was reachable from the public internet when it only ever needed to be reached from one place. Fixed by binding it to localhost and reaching it through the same authenticated path as everything else, after confirming nothing legitimate depended on the old access pattern.
Password-based SSH under constant brute-force attempts — the auth log showed tens of thousands of failed login attempts, which is normal internet background noise, not evidence of a breach on its own. The real question was whether any successful logins fell outside known-good patterns (they didn't) — then password auth was switched off entirely.
fail2ban installed but not enabled — the package was on disk from a previous setup step, but the systemd service had never actually been started. A one-line fix, easy to miss without explicitly checking service state instead of trusting that "installed" means "running."
No rate limiting on public form endpoints — a contact-style form had no throttling in front of it at all, making it a free target for scripted abuse. Fixed with an Nginx limit_req_zone, the same pattern shown in the reverse-proxy example on the Networking page.

The pattern that held across all five: don't patch an exposure until you know exactly who's been using it and how. Read the logs first, confirm what legitimately depends on the current behavior, then design the fix so nothing breaks on the way to being secure. The full write-up, with more incidents from other engagements, is on the Network Engineering & Hardening case study.

Layered Defense

No single control is enough on its own — encryption, authentication, rate limiting, and monitoring each cover a different failure mode. That's the same layered approach behind the remote IT & security services offered on this site.