0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
JPPINTO
  • Home
  • Blog
  • Certifications
  • About
  • Contact
  • Shop
  • Gallery
  • Current Setup
Contact

Search

July 5, 2026 / Linux, Servers, Ubuntu

Configure UFW Firewall on Ubuntu for Web Servers

Tags: firewall, linux, server security, server setup, ssh, ubuntu, ufw
Featured image for Configure UFW Firewall on Ubuntu for Web Servers

UFW, short for Uncomplicated Firewall, is a simple firewall management tool for Ubuntu. It is a good fit for web servers because the usual rule set is easy to reason about: allow web traffic, restrict SSH, deny unexpected inbound connections, and keep outbound traffic working.

This guide walks through a deployment script that configures UFW for a typical Ubuntu web server.

The example IP addresses in this article use documentation-safe ranges such as 203.0.113.10, 198.51.100.20, and 192.0.2.15. Replace them with your own trusted SSH source IPs before running the commands on a real server.

What the Script Does

The Configure-UFWFirewall.sh script:

  • Installs ufw if it is missing.
  • Sets the default incoming policy to deny.
  • Sets the default outgoing policy to allow.
  • Opens HTTP on 80/tcp.
  • Opens HTTPS on 443/tcp.
  • Removes broad SSH rules such as OpenSSH, ssh, and unrestricted 22/tcp.
  • Adds SSH allow rules only for configured source IPs.
  • Enables UFW with ufw --force enable.
  • Prints the final verbose UFW status.
  • Writes a timestamped log under /opt/DevOps/Logs.

That pattern is useful for public web servers because ports 80 and 443 need to be reachable by everyone, but SSH should normally be limited to trusted administrator locations.

Important SSH Warning

Be careful with firewall changes over SSH.

If you enable UFW while your current client IP is not in the SSH allowlist, you can lock yourself out of the server. Before applying the script, confirm the public IP address you are connecting from and include it in the allowlist.

If possible, keep a cloud console, hypervisor console, or recovery session open while changing firewall rules.

Preview the Changes First

Start with a dry run:

sudo bash /opt/DevOps/Scripts/ServerDeployment/Configure-UFWFirewall.sh --dry-run

The dry run shows the commands the script would execute without changing the active firewall.

Run with a Custom SSH Allowlist

Use --ssh-ips when you want to replace the default SSH allowlist:

sudo bash /opt/DevOps/Scripts/ServerDeployment/Configure-UFWFirewall.sh \
  --ssh-ips "203.0.113.10,198.51.100.20"

In this example:

  • 203.0.113.10 represents one trusted administrator IP.
  • 198.51.100.20 represents another trusted administrator IP.

Use your actual trusted IPs instead.

You can also add individual IPs with repeated --ssh-ip options:

sudo bash /opt/DevOps/Scripts/ServerDeployment/Configure-UFWFirewall.sh \
  --ssh-ip 203.0.113.10 \
  --ssh-ip 198.51.100.20

Run the Default Configuration

If the script defaults have already been reviewed and adjusted for your environment, run:

sudo bash /opt/DevOps/Scripts/ServerDeployment/Configure-UFWFirewall.sh

For a real server, do not rely on sample IPs from an article. Put your own trusted SSH source IPs into the command or into the script configuration.

Use a Non-Standard SSH Port

If SSH listens on a port other than 22, pass it explicitly:

sudo bash /opt/DevOps/Scripts/ServerDeployment/Configure-UFWFirewall.sh \
  --ssh-port 2222 \
  --ssh-ips "203.0.113.10,198.51.100.20"

Only do this if your SSH daemon is actually listening on that port.

Check the UFW Service

After applying the firewall rules, check the service:

sudo systemctl status ufw

You want UFW to be installed, enabled, and active.

Check the Firewall Rules

Show the verbose status:

sudo ufw status verbose

You should expect a shape like this:

Status: active
Logging: on
Default: deny (incoming), allow (outgoing), disabled (routed)

To                         Action      From
--                         ------      ----
80/tcp                     ALLOW IN    Anywhere
443/tcp                    ALLOW IN    Anywhere
22/tcp                     ALLOW IN    203.0.113.10
22/tcp                     ALLOW IN    198.51.100.20

The important part is that SSH is limited to the trusted source IPs instead of being open to Anywhere.

Show Numbered Rules

Numbered rules are helpful when you need to delete or inspect a specific rule:

sudo ufw status numbered

Example output:

Status: active

     To                         Action      From
     --                         ------      ----
[ 1] 80/tcp                     ALLOW IN    Anywhere
[ 2] 443/tcp                    ALLOW IN    Anywhere
[ 3] 22/tcp                     ALLOW IN    203.0.113.10
[ 4] 22/tcp                     ALLOW IN    198.51.100.20

If you see an unrestricted SSH rule such as 22/tcp ALLOW IN Anywhere, remove it only after confirming your source-specific SSH rules are correct.

Confirm Listening Services

UFW controls access, but it is still useful to confirm which services are listening:

sudo ss -tulpn | grep -E ':22|:80|:443'

Typical output might show:

tcp   LISTEN 0 4096 0.0.0.0:22    0.0.0.0:* users:(("sshd",pid=1234,fd=3))
tcp   LISTEN 0 4096 0.0.0.0:80    0.0.0.0:* users:(("nginx",pid=2345,fd=6))
tcp   LISTEN 0 4096 0.0.0.0:443   0.0.0.0:* users:(("nginx",pid=2345,fd=7))

If Nginx is not listening on 80 or 443, check the Nginx service and configuration separately.

Full Script

Here is the full Bash script used for this UFW firewall workflow. The SSH allowlist IPs have been replaced with documentation-safe examples; replace them with your own trusted IPs before running it:

#!/usr/bin/env bash

__show_script_usage() {
  cat <<'__SCRIPT_USAGE__'
# Configure-UFWFirewall.sh

Installs/enables UFW, opens HTTP/HTTPS publicly, and restricts SSH to a
configured allowlist.

## Example Usage

Apply the default policy:

```bash
sudo bash /opt/DevOps/Scripts/ServerDeployment/Configure-UFWFirewall.sh
```

Preview changes without applying them:

```bash
sudo bash /opt/DevOps/Scripts/ServerDeployment/Configure-UFWFirewall.sh --dry-run
```

Use a custom SSH allowlist:

```bash
sudo bash /opt/DevOps/Scripts/ServerDeployment/Configure-UFWFirewall.sh \
  --ssh-ips "203.0.113.10,198.51.100.20,192.0.2.15"
```

Add one or more SSH IPs to the defaults:

```bash
sudo bash /opt/DevOps/Scripts/ServerDeployment/Configure-UFWFirewall.sh \
  --ssh-ip 203.0.113.10 \
  --ssh-ip 198.51.100.20
```

## Defaults

- Public inbound: `80/tcp`, `443/tcp`
- SSH port: `22/tcp`
- SSH allowlist: `203.0.113.10`, `198.51.100.20`
- Incoming default policy: deny
- Outgoing default policy: allow

## Notes

The script removes broad SSH allow rules such as `OpenSSH`, `ssh`, and
`22/tcp`, then adds source-specific SSH allow rules. Run from a console or from
an SSH session whose client IP is included in the allowlist.
__SCRIPT_USAGE__
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  case "${1:-}" in
    -h|--help|--usage)
      __show_script_usage
      exit 0
      ;;
  esac
fi

set -euo pipefail

SCRIPT_LOG_ROOT="${SCRIPT_LOG_ROOT:-/opt/DevOps/Logs}"
LOG_FILE="${SCRIPT_LOG_ROOT}/Configure-UFWFirewall.$(date '+%Y.%m.%d.%H%M%S').log"

DEFAULT_SSH_ALLOWED_IPS=(
    "203.0.113.10"
    "198.51.100.20"
)

SSH_ALLOWED_IPS=("${DEFAULT_SSH_ALLOWED_IPS[@]}")
SSH_PORT="${SSH_PORT:-22}"
DRY_RUN="false"
INSTALL_UFW="true"

while [ "$#" -gt 0 ]; do
    case "$1" in
        --ssh-ip)
            if [ "$#" -lt 2 ] || [[ "${2:-}" == --* ]]; then
                echo "ERROR: --ssh-ip requires an IP address." >&2
                exit 1
            fi
            SSH_ALLOWED_IPS+=("${2:-}")
            shift 2
            ;;
        --ssh-ips)
            if [ "$#" -lt 2 ] || [[ "${2:-}" == --* ]]; then
                echo "ERROR: --ssh-ips requires a comma- or space-separated list." >&2
                exit 1
            fi
            IFS=', ' read -r -a SSH_ALLOWED_IPS <<< "${2:-}"
            shift 2
            ;;
        --ssh-port)
            if [ "$#" -lt 2 ] || [[ "${2:-}" == --* ]]; then
                echo "ERROR: --ssh-port requires a port number." >&2
                exit 1
            fi
            SSH_PORT="${2:-}"
            shift 2
            ;;
        --no-install)
            INSTALL_UFW="false"
            shift
            ;;
        --dry-run)
            DRY_RUN="true"
            shift
            ;;
        *)
            echo "ERROR: Unknown argument: $1" >&2
            exit 1
            ;;
    esac
done

mkdir -p "${SCRIPT_LOG_ROOT}"
exec > >(tee -a "${LOG_FILE}") 2>&1

log() {
    printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}

fail() {
    log "ERROR: $*"
    exit 1
}

run() {
    if [ "${DRY_RUN}" = "true" ]; then
        log "DRY RUN: $*"
        return 0
    fi

    "$@"
}

require_command() {
    local command_name="$1"
    if ! command -v "${command_name}" >/dev/null 2>&1; then
        fail "Required command not found: ${command_name}"
    fi
}

validate_port() {
    local port="$1"
    if ! [[ "${port}" =~ ^[0-9]+$ ]] || [ "${port}" -lt 1 ] || [ "${port}" -gt 65535 ]; then
        fail "Invalid SSH port: ${port}"
    fi
}

validate_ip_or_cidr() {
    local value="$1"
    if ! [[ "${value}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?$ ]]; then
        fail "Invalid IPv4/CIDR value: ${value}"
    fi
}

dedupe_ips() {
    local ip
    local deduped=()
    local seen=" "

    for ip in "${SSH_ALLOWED_IPS[@]}"; do
        [ -n "${ip}" ] || continue
        if [[ "${seen}" != *" ${ip} "* ]]; then
            deduped+=("${ip}")
            seen+="${ip} "
        fi
    done

    SSH_ALLOWED_IPS=("${deduped[@]}")
}

delete_broad_ssh_rules() {
    local broad_rules=(
        "allow OpenSSH"
        "limit OpenSSH"
        "allow ssh"
        "limit ssh"
        "allow ${SSH_PORT}/tcp"
        "limit ${SSH_PORT}/tcp"
    )
    local rule

    log "Removing broad SSH allow/limit rules if present."
    for rule in "${broad_rules[@]}"; do
        if [ "${DRY_RUN}" = "true" ]; then
            log "DRY RUN: ufw delete ${rule} || true"
        else
            # These delete commands are idempotent enough for UFW automation:
            # missing rules return non-zero and are ignored.
            ufw delete ${rule} >/dev/null 2>&1 || true
        fi
    done
}

apply_firewall_rules() {
    local ip

    log "Applying UFW defaults."
    run ufw default deny incoming
    run ufw default allow outgoing

    log "Allowing HTTP and HTTPS publicly."
    run ufw allow 80/tcp
    run ufw allow 443/tcp

    delete_broad_ssh_rules

    log "Allowing SSH only from configured source IPs."
    for ip in "${SSH_ALLOWED_IPS[@]}"; do
        log "Allow SSH from ${ip} to port ${SSH_PORT}/tcp"
        run ufw allow from "${ip}" to any port "${SSH_PORT}" proto tcp
    done

    log "Enabling UFW."
    run ufw --force enable
}

show_status() {
    if [ "${DRY_RUN}" = "true" ]; then
        log "DRY RUN: ufw status verbose"
        return 0
    fi

    ufw status verbose
}

if [ "${EUID}" -ne 0 ] && [ "${DRY_RUN}" != "true" ]; then
    fail "Run with sudo, or use --dry-run for preview."
fi

validate_port "${SSH_PORT}"
dedupe_ips

if [ "${#SSH_ALLOWED_IPS[@]}" -eq 0 ]; then
    fail "SSH allowlist cannot be empty."
fi

for ip in "${SSH_ALLOWED_IPS[@]}"; do
    validate_ip_or_cidr "${ip}"
done

log "Log file: ${LOG_FILE}"
log "SSH port: ${SSH_PORT}/tcp"
log "SSH allowed IPs: ${SSH_ALLOWED_IPS[*]}"
log "Public ports: 80/tcp 443/tcp"
log "Dry run: ${DRY_RUN}"

if ! command -v ufw >/dev/null 2>&1; then
    if [ "${INSTALL_UFW}" != "true" ]; then
        fail "ufw is not installed and --no-install was passed."
    fi

    log "Installing UFW."
    if [ "${DRY_RUN}" = "true" ]; then
        log "DRY RUN: apt update"
        log "DRY RUN: apt install -y ufw"
    else
        require_command apt
        apt update
        apt install -y ufw
    fi
fi

if [ "${DRY_RUN}" != "true" ]; then
    require_command ufw
fi
apply_firewall_rules
show_status

log "UFW firewall configuration complete."

Troubleshooting

If UFW is inactive, check:

sudo systemctl status ufw
sudo ufw status verbose

If SSH access is failing from a trusted location, confirm the IP in the rule matches the public IP your connection is coming from:

curl -4 ifconfig.me

Then add the missing source IP from a console session:

sudo ufw allow from 192.0.2.15 to any port 22 proto tcp
sudo ufw status numbered

If you need to temporarily disable UFW from a recovery console:

sudo ufw disable

Use that only as a recovery step. The better fix is to add the correct source-specific SSH rule and re-enable UFW.

Quick Reference

sudo bash /opt/DevOps/Scripts/ServerDeployment/Configure-UFWFirewall.sh --dry-run

sudo bash /opt/DevOps/Scripts/ServerDeployment/Configure-UFWFirewall.sh \
  --ssh-ips "203.0.113.10,198.51.100.20"

sudo systemctl status ufw
sudo ufw status verbose
sudo ufw status numbered
sudo ss -tulpn | grep -E ':22|:80|:443'

A firewall script should be boring, predictable, and easy to verify. For a web server, that means public HTTP and HTTPS, restricted SSH, denied unexpected inbound traffic, and a simple set of commands to prove the final state.

Post Views: 25
<- Create an Nginx Default Catch-All Site on Ubuntu
Transfer S3 Contents Between Buckets with PowerShell ->

Categories

  • Active Directory (5)
  • AI (2)
  • Amazon Cloud Services (1)
  • AWS (2)
  • Blazor (1)
  • C# (C-Sharp) (3)
  • CI/CD Pipelines (1)
  • Cloud (1)
  • Containers (4)
  • Deployment (2)
  • Development (4)
  • Docker (3)
  • General (5)
  • IIS 6.0 (4)
  • IIS 7.0 (10)
  • IIS 8.0 (1)
  • Infrastructure as Code (IaC) (1)
  • Kubernetes (3)
  • Linux (9)
  • Microsoft 365 (2)
  • MySQL (1)
  • Office 2010 (1)
  • PHP (1)
  • PowerShell (8)
  • Productivity (1)
  • Servers (9)
  • SharePoint 2007 (8)
  • SharePoint 2010 (19)
  • SharePoint 2013 (2)
  • SharePoint Online (1)
  • SMTP (4)
  • SQL Server 2008 (1)
  • SQL Server 2008 R2 (1)
  • SQL Server 2012 (2)
  • SQL Server 2019 (1)
  • Troubleshooting (1)
  • Ubuntu (9)
  • Uncategorized (1)
  • URL Rewrite (2)
  • Visual Studio 2019 (1)
  • Visual Studio Code (1)
  • Windows 10 (6)
  • Windows 2003 (9)
  • Windows 2008 (18)
  • Windows 2012 (6)
  • Windows 7 (3)
  • Windows Firewall (1)
  • Windows Vista (1)
  • WordPress (3)
  • WP-CLI (3)

Recent Posts

  • Entering .com in Bucket Names Causes SSL Errors
  • Transfer S3 Contents Between Buckets with PowerShell
  • Configure UFW Firewall on Ubuntu for Web Servers
  • Create an Nginx Default Catch-All Site on Ubuntu
  • Install and Configure Redis on Ubuntu for Local Object Cache

Advertisement

Tags

ai coding agents aws bash cloud storage developer workflow dev to production externalize blob externalize sharepoint data filezilla server firewall rules filazilla full installation http redirect https IIS iis7 iis 7 installation IIS installation index server configuration installing cumulative updates linux load balance central administration microsoft 365 nginx powerpoint powershell redirect http to https s3 server setup sharepoint 2010 cumulative updates sharepoint 2010 farm build sharepoint 2010 farm configuration sharepoint 2010 farm installation sharepoint data externalization SMTP ssl storagepoint ubuntu web server windows Windows 7 windows firewall configuration windows server 2008 wordpress wp-cli x86
© 2026 JPPinto.com. All rights reserved.