Skip to main content

Secure Web Application Gateway

The Secure Web Application Gateway (a.k.a. SWAG) is a reverse proxy service. It serves a reverse proxy for all the web apps on kasad.com.

It runs as a Docker container using the lscr.io/linuxserver/swag:latest image.

Access

The SWAG container runs inside a NAT-protected LAN with a dynamic public IP. This presents some problems when hosting a public-facing webserver.

Instead of traditional port forwarding, we use Cloudflare's Zero Trust Platform. It allows us to connect the SWAG container to Cloudflare's network using a Tunnel. Cloudflare Tunnels are egress-only, meaning no incoming connections will be established, so no port forwarding or dynamic DNS solutions are required.

Configuration

Docker mods

The SWAG container uses so-called mods to add extra features. Our SWAG container uses:

  • universal-cloudflared - Allows SWAG to connect to the Cloudflare Zero Trust platform
  • swag-dashboard - Provides a dashboard displaying information and metrics about SWAG
  • swag-auto-reload - Automatically reloads NGINX when configuration files are modified

TLS Certificates

The SWAG container uses Certbot to obtain signed TLS certificates from Let's Encrypt. To configure this, we define several environment variables for the contianer:

URL: kasad.com
SUBDOMAINS: swag,auth,tasks,books,bw
ONLY_SUBDOMAINS: true
VALIDATION: dns
DNSPLUGIN: cloudflare
PROPAGATION: 30
EMAIL: admin@kasad.com

These settings configure Certbot to add a temporary DNS record to kasad.com to verify ownership of the domain, then wait 30 seconds for propagation, then request a certificate valid only for the subdomains specified. The email address provided is optional and is used for certificate expiration notifications.

Cloudflare Tunnel parameters

The Cloudflare tunnel connection is configured using ennvironment variables. There is also a configuration file which handles the routing rules for incoming traffic.

CF_ZONE_ID: [redacted]
CF_ACCOUNT_ID: [redacted]
CF_API_TOKEN: [redacted]
CF_TUNNEL_NAME: swag.kasad.com
CF_TUNNEL_PASSWORD: [redacted]
FILE__CF_TUNNEL_CONFIG: /config/tunnelconfig.yml

Most of the parameters are potentially sensitive API keys.

CF_API_TOKEN must contain an API token which has the Account > Cloudflare Tunnel > Edit permission annd the Zone > DNS > Edit permission for the kasad.com zone.

The CF_ZONE_ID and CF_ACCOUNT_ID can be found on the Overview page for a zone in the Cloudflare dashboard.

CF_TUNNEL_NAME is the name for the Tunnel that will be created. CF_TUNNEL_PASSWORD is a string that can be made up or randomly generated. It should be at least 32 characters.

Ingress routing

A single tunnel is used for multiple subdomains, so cloudflared needs to know where to route traffic for each origin. This is done using a YAML configuration file following Cloudflare's specification.

The YAML contents can either (1) be specified directly as the value for the CF_TUNNEL_CONFIG environment variable, or (2) be placed in a file inside the container. The file path must then be specified in the FILE__CF_TUNNEL_CONFIG environment variable. We use the second option.

The contents of the config/tunnelconfig.yml are:

ingress:
  - hostname: swag.kasad.com
    service: https://swag.kasad.com
  - hostname: auth.kasad.com
    service: https://auth.kasad.com
  - hostname: tasks.kasad.com
    service: https://tasks.kasad.com
  - hostname: books.kasad.com
    service: https://books.kasad.com
  - hostname: bw.kasad.com
    service: https://bw.kasad.com
  - hostname: send.kasad.com
    service: https://send.kasad.com
  - service: http_status:404
Hostname routing

You'll notice in the config/tunnelconfig.yml file that the service field has the same hostname as the hostname field. This is for a reason.

Since the SWAG's NGINX server uses the Host field of requests to route traffic, the Host header on incoming requests must stay intact. This means that cloudflared needs to be able to access the NGINX instance using the DNS domain for the subdomains.

This just means we need to define extra hostnames for the SWAG container which all point to localhost. This can easily be done in the Docker Compose file:

services:
  swag:
    # ...
    extra_hosts:
      - swag.kasad.com:127.0.0.1
      - auth.kasad.com:127.0.0.1
      - tasks.kasad.com:127.0.0.1
      - books.kasad.com:127.0.0.1
      - bw.kasad.com:127.0.0.1
      - send.kasad.com:127.0.0.1

LAN access

If on the same LAN as the host for the SWAG container, the SWAG can be accessed using the IP address of the host without having to access it through the public Cloudflare IP.

To disable this, remove the port forwarding definitions from the Compose file:

services:
  swag:
    # ...
    ports:
      - '80:80'
      - '443:443'

Docker network

Since the SWAG container needs network access to any services it is reverse-proxying, the upstream containers must be on the same (Docker) network as the SWAG container. This does not require any extra configuration in the SWAG stack, but it does require the following configuration in the Compose file for any upstream services:

services:
  # ...
  
  some_upstream_service:
    # ...
    networks:
      - default # The default network for this service's stack
      - swag # The SWAG stack's network
      
networks:
  swag:
    external: true # Tells Docker to look for an existing network instead of creating a new one
    name: swag_default # This is the name of the default network for the SWAG stack

Service configuration

The SWAG container runs NGINX as the reverse proxy webserver. Its configuration files are hosted in config/nginx/. Configuration for each service that is being reverse-proxied exists under config/nginx/proxy-confs/. See the README.md file in that directory for details.

SWAG comes with sample configs for many services. These samples are files in config/nginx/proxy-confs/ with the names <service>.<type>.conf.sample. <service> is the name of the service. <type> is either subdomain or subfolder, depending on how the service is reverse-proxied.

Adding new subdomains

Hosting a service on a new subdomain requires additional steps past just the NGINX config. A new TLS certificate must be obtained and a new DNS record must be added, along with routing rules.

Luckily, this is easy. Simply add the new subdomain to the SUBDOMAINS environment variable for the SWAG container and the extra_hosts list:

services:
  swag:
    # ...
    environment:
      SUBDOMAINS: swag,auth,...,newsub
      # ...
    extra_hosts:
      # ...
      - newsub.kasad.com:127.0.0.1

Then add a new entry in the config/tunnelconfig.yml file:

ingress:
  # ...
  - hostname: newsub.kasad.com
    service: https://newsub.kasad.com
  # ...

Finally, reload the SWAG stack:

# systemctl reload docker-compose@-srv-swag.service

Deployment

The Secure Web Application Gateway runs as just a single Docker container. Since we're running Heimdall as a dashboard/landing page, the SWAG and Heimdall containers are run in the same Compose stack.

Docker Compose service configuration for the SWAG container:

version: '3'

services:
  swag:
    image: lscr.io/linuxserver/swag:latest
    container_name: swag
    environment:
      PUID: 938 # swag
      PGID: 941 # servlets
      UMASK: 007
      URL: kasad.com
      SUBDOMAINS: swag,auth,tasks,books,bw,send
      ONLY_SUBDOMAINS: true
      VALIDATION: dns
      DNSPLUGIN: cloudflare
      PROPAGATION: 30
      EMAIL: admin@kasad.com
      DOCKER_MODS: linuxserver/mods:universal-cloudflared|linuxserver/mods:swag-dashboard|linuxserver/mods:swag-auto-reload
      CF_ZONE_ID: [redacted]
      CF_ACCOUNT_ID: [redacted]
      CF_API_TOKEN: [redacted]
      CF_TUNNEL_NAME: swag.kasad.com
      CF_TUNNEL_PASSWORD: [redacted]
      FILE__CF_TUNNEL_CONFIG: /config/tunnelconfig.yml
      TZ: America/Los_Angeles
    extra_hosts:
      - swag.kasad.com:127.0.0.1
      - auth.kasad.com:127.0.0.1
      - tasks.kasad.com:127.0.0.1
      - books.kasad.com:127.0.0.1
      - bw.kasad.com:127.0.0.1
      - send.kasad.com:127.0.0.1
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./config:/config
    restart: unless-stopped

SWAG Dashboard

We've enabled the linuxserver/mods:swag-dashboard mod for the SWAG container. This provides a dashboard page which displays information and metrics about the SWAG server.

The dashboard endpoint (/dashboard) is protected by a Cloudflare Access policy which allows only authenticated users who belong to the Administrators group.

NGINX configuration

SWAG only comes with a subdomain configuration file for the dashboard, but we want it hosted on swag.kasad.com/dashboard, so we'll need to create our own configuration file.

This file should be saved as config/nginx/proxy-confs/dashboard.subfolder.conf:

location /dashboard {
    alias /dashboard/www;
    index index.php;
    rewrite_log on;
    try_files $uri $uri/ /dashboard/index.php?$args;
}

location ~ ^/dashboard/(.*\.php)$ {
    alias /dashboard/www/$1;
    rewrite_log on;
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index index.php;
    include /etc/nginx/fastcgi_params;
    fastcgi_param DOCUMENT_ROOT /dashboard/www;
    add_header X-Document-Root "$document_root";
}