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";
}
No Comments