Skip to main content
Teksolvr
CybersecurityJuly 4, 20267 min read

Linux/Windows Server Hardening: A Step-by-Step Guide to Secure Configurations

Rudra Chauhan, Senior Systems Architect

Linux/Windows Server Hardening: A Step-by-Step Guide to Secure Configurations

Linux/Windows Server Hardening: A Step-by-Step Guide to Secure Configurations

Securing servers is a continuous process that begins with a solid baseline configuration and evolves with threat intelligence. This guide walks you through hardening the most critical attack surfaces — SSH, TLS, firewalls, and HTTP security headers — on both Linux and Windows platforms, and then ties everything together with a repeatable risk-mitigation workflow.

SSH Daemon Configuration - Best Practices for Secure Connections

Direct answer: Harden sshd by disabling legacy protocols, enforcing key-based authentication, limiting access, and applying strong cryptographic defaults.

Core Hardening Steps (Linux)

bash
# /etc/ssh/sshd_config – replace the entire file or append these lines
Port 2222                                 # non-standard port reduces automated scans
Protocol 2
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
AuthenticationMethods publickey
AllowUsers alice bob@192.0.2.0/24          # restrict by user and source CIDR
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
HostKeyAlgorithms ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com

Reload: systemctl reload sshd

Windows OpenSSH Server (Win32-OpenSSH)

powershell
# C:\ProgramData\ssh\sshd_config
Port 2222
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
AuthenticationMethods publickey
AllowUsers alice,bob
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256

Restart the service: Restart-Service sshd

Quick Reference Table – SSH Hardening Parameters

ParameterRecommended ValueReason
Port2222 (or any >1024)Reduces noise from bot scanners
PermitRootLoginnoPrevents direct root compromise
PasswordAuthenticationnoForces key-based auth
AuthenticationMethodspublickeyGuarantees MFA-style control
Cipherschacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.comModern AEAD ciphers only
KexAlgorithmscurve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256Forward-secure key exchange
AllowUsersalice bob@192.0.2.0/24Least-privilege network segmentation

Tool tip: Generate a ready-to-paste sshd_config with the SSH Config Generator and validate syntax via sshd -t.


Secure SSL/TLS Configuration - Implementing Best Practices for Encryption

Direct answer: Deploy TLS 1.2 + 1.3 only, use strong cipher suites, enable HSTS, OCSP stapling, and enforce certificate transparency.

Nginx Example (Linux)

nginx
# /etc/nginx/conf.d/ssl-hardening.conf
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Protocol & Cipher Suite
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256';
    ssl_prefer_server_ciphers off;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;

    # HSTS (1 year, include subdomains, preload)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Security Headers (see next section)
    include /etc/nginx/security-headers.conf;
}

IIS (Windows) – PowerShell Hardening

powershell
# Disable TLS 1.0/1.1, enable 1.2/1.3
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server' -Name 'Enabled' -Value 0
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server' -Name 'Enabled' -Value 0
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Name 'Enabled' -Value 1
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.3\Server' -Name 'Enabled' -Value 1

# Restrict cipher suites (example: only AES-GCM & CHACHA20)
$ciphers = @(
    'TLS_AES_256_GCM_SHA384',
    'TLS_CHACHA20_POLY1305_SHA256',
    'TLS_AES_128_GCM_SHA256'
)
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Ciphers' -Name 'Functions' -Value ($ciphers -join ',')

# Enable HSTS via web.config (add to site root)
<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains; preload" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>
</configuration>

Reference

  • IETF RFC 7519 – JSON Web Token (JWT) used for stateless auth; ensure tokens are transmitted only over TLS-protected channels.

Firewall Rules - Configuring iptables, nftables, ufw, and Windows Firewall for Secure Network Access

Direct answer: Adopt a default-deny posture, allow only required inbound ports (SSH, HTTPS, management), and log dropped packets for audit.

Linux – nftables (modern, atomic)

bash
#!/usr/sbin/nft -f
flush ruleset

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        iif "lo" accept
        ct state established,related accept
        # SSH (custom port)
        tcp dport 2222 ct state new limit rate 5/minute accept
        # HTTP/HTTPS
        tcp dport {80,443} accept
        # ICMP (rate-limited)
        icmp type echo-request limit rate 1/second accept
        # Log & drop
        log prefix "DROP_IN: " level info
    }
    chain forward {
        type filter hook forward priority 0; policy drop;
    }
    chain output {
        type filter hook output priority 0; policy accept;
    }
}

Linux – UFW (Ubuntu/Debian friendly)

bash
ufw default deny incoming
ufw default allow outgoing
ufw allow 2222/tcp comment 'SSH hardened port'
ufw allow 80,443/tcp comment 'Web traffic'
ufw limit 2222/tcp comment 'Rate-limit SSH'
ufw enable

Windows Firewall – PowerShell (Domain/Private profiles)

powershell
# Reset to baseline
Set-NetFirewallProfile -All -DefaultInboundAction Block -DefaultOutboundAction Allow -NotifyDisplayEnabled False

# Allow SSH (custom port)
New-NetFirewallRule -DisplayName "Allow SSH 2222" -Direction Inbound -Protocol TCP -LocalPort 2222 -Action Allow -Profile Domain,Private -Enabled True

# Allow HTTP/HTTPS
New-NetFirewallRule -DisplayName "Allow HTTP/HTTPS" -Direction Inbound -Protocol TCP -LocalPort 80,443 -Action Allow -Profile Domain,Private -Enabled True

# Enable logging for dropped packets
Set-NetFirewallProfile -All -LogFileName "%systemroot%\system32\LogFiles\Firewall\pfirewall.log" -LogMaxSizeKilobytes 4096 -LogAllowed False -LogBlocked True -LogIgnored True

Tool tip: Use the Firewall Rule Generator to produce the exact iptables, nftables, ufw, or Windows Firewall commands for your environment.


Security Headers - Implementing CSP, HSTS, and Other Essential Headers for Secure Web Applications

Direct answer: Deploy a defense-in-depth header set on every HTTP response: Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, and Cross-Origin-Opener-Policy.

Nginx Snippet (/etc/nginx/security-headers.conf)

nginx
# Content Security Policy – adjust sources to your assets
add_header Content-Security-Policy
    "default-src 'self';
     script-src 'self' 'nonce-$request_id' https://cdn.example.com;
     style-src  'self' 'nonce-$request_id' https://fonts.googleapis.com;
     img-src    'self' data: https://cdn.example.com;
     font-src   'self' https://fonts.gstatic.com;
     connect-src 'self' https://api.example.com;
     frame-ancestors 'none';
     base-uri 'self';
     form-action 'self';"
    always;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;

Apache (/etc/apache2/conf-available/security-headers.conf)

apache
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-%{UNIQUE_ID}e' https://cdn.example.com; style-src 'self' 'nonce-%{UNIQUE_ID}e' https://fonts.googleapis.com; img-src 'self' data: https://cdn.example.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Resource-Policy "same-origin"

IIS – web.config (add under <system.webServer>)

xml
<httpProtocol>
  <customHeaders>
    <add name="Content-Security-Policy" value="default-src 'self'; script-src 'self' 'nonce-{RANDOM}' https://cdn.example.com; style-src 'self' 'nonce-{RANDOM}' https://fonts.googleapis.com; img-src 'self' data: https://cdn.example.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" />
    <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains; preload" />
    <add name="X-Content-Type-Options" value="nosniff" />
    <add name="X-Frame-Options" value="DENY" />
    <add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
    <add name="Permissions-Policy" value="geolocation=(), microphone=(), camera=()" />
    <add name="Cross-Origin-Opener-Policy" value="same-origin" />
    <add name="Cross-Origin-Resource-Policy" value="same-origin" />

Troubleshooting Checklist

SSH

Was this guide helpful?

Troubleshooting or testing this guide?

Teksolvr provides 97 free tools to help you inspect DNS configs, validate DKIM certificates, test port openings, check server blacklists, and run calculations.