freenginx 1.31.4 on AWS User Guide
Overview
This guide covers launching and verifying freenginx 1.31.4 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other VPN, proxy and remote access servers in the listing are described on the listing's guide hub.
freenginx is the community-maintained fork of the nginx web server, reverse proxy and load balancer, led by long-time nginx core developer Maxim Dounin. It uses the same configuration language as nginx, so existing nginx server blocks, proxy_pass setups and how-tos work unchanged.
On this image freenginx serves HTTPS on port 443 with a certificate generated on your instance's first boot, and port 80 only redirects to HTTPS. Out of the box it serves a cloudimg landing page and reverse-proxies the /backend/ path to a small demo backend on 127.0.0.1:8081, so you can see both jobs working before you put your own site or servers behind it.
cloudimg compiles freenginx 1.31.4 from the official source release after checking its checksum and its PGP signature by the freenginx maintainer, then removes the compiler again. The source, signature, build options and binary checksum are recorded in /usr/share/doc/freenginx/PROVENANCE.txt.
Why 1.31.4 and not the 1.30 stable branch: freenginx 1.31.4 (September 2026) carries fixes that the 1.30.1 stable release (June 2026) does not, including buffer overflows that a configuration using map with regular expressions, or gRPC proxying, could expose, and extra overrun checks in variable handling. The 1.30 branch has had no update since June, so cloudimg ships the release with the fixes.
What is on the image:
- freenginx 1.31.4 (
nginx.service) as/usr/sbin/nginx, with the familiar Ubuntu layout:/etc/nginx/nginx.conf,conf.d/,sites-available/andsites-enabled/,snippets/ - HTTPS on 443 (TLS 1.2 and 1.3, HTTP/2) with a self-signed certificate generated on your instance's first boot; port 80 only redirects to HTTPS
- A landing page in
/var/www/htmland a demo backend on127.0.0.1:8081, the target of/backend/ - Helpers:
freenginx-selftestandfreenginx-tls-cert(certificate and served names) - Daily log rotation (
/etc/logrotate.d/nginx), and an apt pin so Ubuntu's ownnginxpackages can never replace freenginx - No metadata access: freenginx cannot reach the instance metadata service
Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3
Security posture
- No default credential and no shared secret. freenginx has no login of its own: the landing page and
/backend/are public web content by design. The only secret on the instance is the TLS private key, which first boot generates for this instance only (mode 0600, ownerroot). No key or certificate is baked into the image. - Only your names are served. freenginx answers only the names first boot read from the instance metadata (public IPv4 address, public DNS name, private IPv4 address and private DNS name) and
localhost. A request for any other host name, or with no host name at all, is dropped without a response (nginx's444) on ports 80 and 443 and never reaches a backend. - Client addresses come from the connection, never from headers. No
real_iptrust is configured, and the shipped proxy settings overwriteX-Forwarded-ForandX-Real-IPwith the real peer address (never appending what the client sent) and removeForwarded,X-Forwarded-Hostand other client-address headers before a request reaches your backend. - No forward proxy, no outbound calls. Every
proxy_passtarget is a fixed address, never taken from the request, so a request for another site (GET http://other-host/...orCONNECT) is refused. OCSP stapling and the resolver are off, so freenginx makes no connection of its own. - No SSRF to instance credentials.
nginx.servicecarries anIPAddressDenyrule for the metadata addresses (169.254.169.254, fd00:ec2::254, 168.63.129.16), so the proxy can never fetch instance credentials, whatever backend it is pointed at. The start-up check also refuses a configuration that names the metadata service. Nothing broader is blocked: your backends can be anywhere. The AMI is registered IMDSv2 only. - Modern TLS. TLS 1.2 and 1.3 only, ECDHE key exchange with AEAD ciphers only, TLS session tickets off, HTTP/2 on.
server_tokens off(theServerheader saysfreenginxwith no version), directory listings off,X-Content-Type-Options: nosniff,X-Frame-Options: SAMEORIGIN. HSTS is ready to switch on (one line) once you have a CA-signed certificate. - Least privilege. The master process binds 80 and 443 with a small set of capabilities and every worker runs as the unprivileged
nginxuser with none; the service runs with a read-only/usrand/etc, a private/tmpand no new privileges. - Hardened build. The binary is position-independent with full RELRO and a non-executable stack, links only Ubuntu 24.04 main libraries (OpenSSL 3, PCRE2, zlib), and no compiler or development package is left on the image.
- Fails closed. freenginx starts only after first boot has written this instance's key, certificate and names and a ready marker, and every start and reload runs a check and
nginx -tfirst. If first boot cannot finish, freenginx stays stopped and first boot tries again on the next boot. - Per-instance identity. SSH host keys and the machine ID are generated fresh on every instance.
Security notes for freenginx 1.31.4. cloudimg checked every 2025 and 2026 nginx security advisory against the freenginx code. The ones that affect the shared code are fixed in 1.31.4 or concern modules this image does not include (HTTP/3, mail, stream, WebDAV, mp4, flv, slice). One advisory, a use-after-free when asynchronous file I/O is combined with server-side includes (CVE-2026-97205), has no freenginx release yet, so file AIO and thread pools are not compiled in: the aio directive is refused by nginx -t on this image. Two October 2026 advisories describe buffer overflows that need specific configurations (alias with rewrite and $request_filename, or map regular-expression captures used in root or log_format); they are not in the shipped configuration, and 1.31.4's added overrun checks stop such a request with an error instead of an overflow. Keep ssl_session_tickets off (the default here) unless you also set your own ssl_session_ticket_key files. cloudimg ships an updated version of this listing when freenginx publishes a new release.
Prerequisites
- An AWS account and permission to launch EC2 instances
- An EC2 key pair in the Region you launch in
- Optionally, the site files or backend servers you want freenginx to serve
Recommended instance type: m5.large. freenginx is light; size the instance for your traffic.
Connecting to your instance
| Version | Operating system | SSH user |
|---|---|---|
| freenginx 1.31.4 on Ubuntu 24.04 | Ubuntu 24.04 LTS | ubuntu |
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once the subscription is active, choose Launch your software.
- Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose freenginx 1.31.4 on Ubuntu 24.04, then choose your Region.
- Pick an instance type (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - Security group: the listing's default group allows SSH (22), HTTP (80, redirect only) and HTTPS (443). Restrict SSH to your own IP address.
- Choose your VPC and a public subnet (or attach an Elastic IP later), leave the metadata defaults (the image already requires IMDSv2), then choose Launch.
Option B: AWS CLI
Run this on your own workstation. It creates a security group that allows SSH from your management address and the web ports from everyone, then launches the instance. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDR range with your own:
SG=$(aws ec2 create-security-group --group-name freenginx \
--description "freenginx web server" --vpc-id vpc-0123456789abcdef0 \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 run-instances \
--image-id ami-0123456789abcdef0 \
--instance-type m5.large \
--key-name my-key-pair \
--security-group-ids "$SG" \
--subnet-id subnet-0123456789abcdef0 \
--associate-public-ip-address \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=freenginx}]'
You do not need --metadata-options: the AMI already makes IMDSv2 required.
Step 2: Connect and read the connection details
Connect as ubuntu with the key pair you chose:
ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>
First boot finishes within a minute of launch. It issues the TLS certificate for the instance's addresses, records the names freenginx serves and starts freenginx. The details are in /root/freenginx-endpoints.txt, readable only by root. Nothing in it is a secret:
sudo cat /root/freenginx-endpoints.txt
# freenginx 1.31.4 - generated on first boot by freenginx-firstboot.service. Nothing here is a secret.
# The TLS key in /etc/nginx/ssl was generated on this instance; none was baked into the image. freenginx has no
# login of its own: the landing page and /backend/ are public web content by design.
FREENGINX_HOST=3.95.26.87
FREENGINX_URL=https://3.95.26.87/
FREENGINX_NAMES=3.95.26.87 ec2-3-95-26-87.compute-1.amazonaws.com 172.31.86.91 ip-172-31-86-91.ec2.internal localhost 127.0.0.1
TLS_CERT_SHA256=BD:43:05:1B:54:E7:93:CD:30:88:7B:14:F4:B4:2F:4A:84:35:FC:14:B9:DC:C5:49:D4:97:44:43:27:A3:7A:87
FREENGINX_CONFIG=/etc/nginx/nginx.conf
FREENGINX_SITE=/etc/nginx/sites-available/default
FREENGINX_NAMES_FILE=/etc/nginx/cloudimg/server-names.conf
FREENGINX_WEBROOT=/var/www/html
# Open TCP 443 (and 80, which only redirects) in the security group to the clients that should reach freenginx.
# Your site: /var/www/html, or a server block in /etc/nginx/sites-available (symlink it into sites-enabled)
# Your backend: proxy_pass under location /backend/ in /etc/nginx/sites-available/default
# Apply a change: sudo nginx -t && sudo systemctl reload nginx
# New name or Elastic IP: sudo freenginx-tls-cert <name-or-ip> ...
# Self-test: sudo freenginx-selftest
# Guide: https://www.cloudimg.co.uk/guides/freenginx-1-31-4-on-ubuntu-24-04-aws/
Step 3: Confirm freenginx is running
freenginx is active and first boot has completed. It listens on ports 80 and 443 on every interface; the demo backend listens on 127.0.0.1 only, and nothing listens on UDP:
systemctl is-active nginx.service freenginx-firstboot.service
nginx -v 2>&1
sudo ss -Htlnp '( sport = :80 or sport = :443 or sport = :8081 )' | awk '{print $4}' | sort -u
sudo ss -Hulnp | grep -c nginx || true
active
active
nginx version: freenginx/1.31.4
0.0.0.0:443
0.0.0.0:80
127.0.0.1:8081
[::]:443
[::]:80
0
Step 4: Run the self-test
The shipped self-test checks the whole path from the instance itself: the workers run unprivileged, the landing page is served over HTTPS for the instance's name and /backend/ traverses the proxy to the demo backend, the certificate served is this instance's, port 80 redirects to HTTPS, a host name the instance does not serve is dropped, forward-proxy requests are refused, the demo backend is refused on the private address, and the instance metadata service is unreachable from inside freenginx:
sudo freenginx-selftest
ok: nginx is active; master root, workers nginx with no capabilities
ok: https://3.95.26.87/ serves the landing page and /backend/ traverses the proxy to the loopback-only demo backend
ok: the certificate served on 443 is this instance's (BD:43:05:1B:54:E7:93:CD...)
ok: port 80 redirects to https://3.95.26.87/; an unknown Host is dropped (444) on 80 and 443
ok: forward-proxy (absolute-URI) requests are refused
ok: the demo backend 8081 is refused on 172.31.86.91
ok: metadata: 169.254.169.254 is unreachable from inside nginx
SELFTEST_OK
Step 5: Send traffic through freenginx
A request for the instance's name over HTTPS returns the landing page over HTTP/2 with Server: freenginx (no version); /backend/ returns the demo backend's page through the reverse proxy; plain HTTP is redirected to HTTPS; a request for a name the instance does not serve gets no response at all (curl reports 000):
H=$(sudo sed -n 's/^FREENGINX_HOST=//p' /root/freenginx-endpoints.txt)
curl -sk -D - -o /tmp/fn-page.html --resolve "$H:443:127.0.0.1" "https://$H/" | grep -iE '^(HTTP|server|x-content-type-options)'
grep -o '<title>[^<]*</title>' /tmp/fn-page.html; rm -f /tmp/fn-page.html
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/backend/" | grep -o 'CLOUDIMG_FREENGINX_BACKEND'
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' -H "Host: $H" http://127.0.0.1/docs
curl -sk -o /dev/null -w '%{http_code}\n' --resolve "www.example.org:443:127.0.0.1" https://www.example.org/ || true
HTTP/2 200
server: freenginx
x-content-type-options: nosniff
<title>freenginx is serving - cloudimg</title>
CLOUDIMG_FREENGINX_BACKEND
301 https://3.95.26.87/docs
000
The demo backend's /echo-headers page shows the forwarding headers it received. Forged X-Forwarded-For and X-Real-IP headers are replaced by the address freenginx saw on the connection (here 127.0.0.1, because the request comes from the instance itself):
H=$(sudo sed -n 's/^FREENGINX_HOST=//p' /root/freenginx-endpoints.txt)
curl -sk --resolve "$H:443:127.0.0.1" -H 'X-Forwarded-For: 203.0.113.9' -H 'X-Real-IP: 203.0.113.9' "https://$H/backend/echo-headers"
x-forwarded-for: 127.0.0.1
x-real-ip: 127.0.0.1
x-forwarded-proto: https
x-forwarded-host:
forwarded:
host: 3.95.26.87
Open https://<public-ip>/ in your own browser: after accepting the self-signed certificate you see the landing page, and its /backend/ link shows the demo backend reached through freenginx.
Step 6: Serve your own site or point the proxy at your servers
The layout is the one Ubuntu's nginx packages use. nginx.conf holds the global settings and includes conf.d/*.conf and sites-enabled/*; the default site is sites-available/default, enabled by a symlink; conf.d/00-cloudimg-default-server.conf holds the catch-all servers that drop unknown host names; reusable pieces are in snippets/:
ls /etc/nginx /etc/nginx/conf.d /etc/nginx/sites-enabled
/etc/nginx:
cloudimg
conf.d
fastcgi.conf
fastcgi_params
koi-utf
koi-win
mime.types
nginx.conf
scgi_params
sites-available
sites-enabled
snippets
ssl
uwsgi_params
win-utf
/etc/nginx/conf.d:
00-cloudimg-default-server.conf
/etc/nginx/sites-enabled:
default
Static files come from root /var/www/html in the HTTPS server of the default site, so the simplest change is to put your site's files in /var/www/html. The reverse proxy is the location /backend/ block, and the headers it sends to your backend come from one snippet:
sudo sed -n '/location \/backend\//,/^ }/p' /etc/nginx/sites-available/default
grep -v '^#' /etc/nginx/snippets/cloudimg-proxy-headers.conf
location /backend/ {
proxy_pass http://127.0.0.1:8081/;
include /etc/nginx/snippets/cloudimg-proxy-headers.conf;
}
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host "";
proxy_set_header X-Forwarded-Port "";
proxy_set_header X-Forwarded-Server "";
proxy_set_header X-Original-Forwarded-For "";
proxy_set_header Forwarded "";
proxy_set_header True-Client-IP "";
proxy_set_header X-Client-IP "";
proxy_set_header Client-IP "";
proxy_set_header CF-Connecting-IP "";
proxy_set_header Connection "";
Change proxy_pass to your own server, or define an upstream block in a file in conf.d/ to load-balance between several and point proxy_pass at it. Include the proxy snippet in every location that proxies, so your backends always receive the real client address. To host another site, add a server block in a new file under sites-available/, give it its own server_name and certificate, and symlink it into sites-enabled/. Then test and reload; the reload is refused if the test fails:
sudo nano /etc/nginx/sites-available/default
sudo nginx -t && sudo systemctl reload nginx
sudo journalctl -u nginx -n 20 --no-pager
Allow traffic from this instance's security group to your servers, and stop exposing them directly. If your backend is behind another proxy in front of freenginx (for example an AWS load balancer), add set_real_ip_from for that proxy's address range only, never for 0.0.0.0/0. The self-test looks for the demo backend's page on /backend/, so once you have replaced it the self-test reports that line as failed; everything else still applies. The directive reference is in the freenginx documentation.
Step 7: Use your own domain name and certificate
freenginx-tls-cert re-issues the self-signed certificate, updates the names the default site serves and reloads freenginx. With no argument it reads the instance's current addresses from the instance metadata (useful after attaching an Elastic IP); with arguments it uses the names you give, for example the DNS name your record points at this instance. The first name becomes the target of the port 80 redirect:
sudo freenginx-tls-cert <www.example.com>
sudo freenginx-selftest | tail -1
The names are written to two small include files:
sudo cat /etc/nginx/cloudimg/server-names.conf /etc/nginx/cloudimg/https-redirect.conf
# cloudimg: written by freenginx-tls-cert on 2026-10-27T09:49:41Z. The names the default site serves (any other Host is
# dropped with 444). Change them with: sudo freenginx-tls-cert <name-or-ip> ...
server_name 3.95.26.87 ec2-3-95-26-87.compute-1.amazonaws.com 172.31.86.91 ip-172-31-86-91.ec2.internal localhost 127.0.0.1;
# cloudimg: written by freenginx-tls-cert on 2026-10-27T09:49:41Z. Port 80 redirects to the first configured name.
return 301 https://3.95.26.87$request_uri;
freenginx-tls-cert always adds localhost and 127.0.0.1, and it restores the previous files if nginx -t fails with the new ones. To use a certificate from a public CA instead, copy its full chain to /etc/nginx/ssl/cloudimg.crt and its key to /etc/nginx/ssl/cloudimg.key (mode 0600, owner root), make sure your DNS name is in server-names.conf (run freenginx-tls-cert with it first), then run sudo nginx -t && sudo systemctl reload nginx. If you use an ACME client with the webroot method, add a location /.well-known/acme-challenge/ with a root to the port 80 server of the default site, above its redirect. Once a CA-signed certificate is in place you can switch on HSTS by removing the # from the add_header Strict-Transport-Security line in sites-available/default; do not enable it with the self-signed certificate.
Step 8: Forward proxy and metadata access are blocked
A request that names the metadata service as its target is dropped (curl reports 000), and nginx.service carries the metadata deny rule; the self-test proved in Step 4 that the metadata service does not answer from inside freenginx:
H=$(sudo sed -n 's/^FREENGINX_HOST=//p' /root/freenginx-endpoints.txt)
curl -sk -o /dev/null -w '%{http_code}\n' --resolve "$H:443:127.0.0.1" --request-target 'http://169.254.169.254/latest/meta-data/' "https://$H/" || true
systemctl show -p IPAddressDeny nginx.service
000
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Even if a proxy_pass line were pointed at 169.254.169.254, the start-up check would refuse the configuration, and the deny rule would block the connection anyway.
Step 9: How the fail-closed start works, and what is compiled in
freenginx starts only once first boot has written /var/lib/cloudimg/freenginx-ready. Every start and every reload runs a check (as root) that refuses to start without this instance's key, certificate and names or with a configuration that names the metadata service, then nginx -t. The modules compiled in are listed by nginx -V:
systemctl cat nginx.service | grep -E '^(ConditionPathExists|ExecStartPre|CapabilityBoundingSet|NoNewPrivileges)='
sudo stat -c '%a %U:%G %n' /etc/nginx/ssl/cloudimg.key /etc/nginx/cloudimg/server-names.conf /root/freenginx-endpoints.txt
nginx -V 2>&1 | tr ' ' '\n' | grep -E '^--with-(http|pcre|compat)'
ConditionPathExists=/var/lib/cloudimg/freenginx-ready
ExecStartPre=+/usr/local/sbin/freenginx-preflight
ExecStartPre=/usr/sbin/nginx -t -q
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID CAP_CHOWN CAP_DAC_OVERRIDE CAP_KILL
NoNewPrivileges=yes
600 root:root /etc/nginx/ssl/cloudimg.key
644 root:root /etc/nginx/cloudimg/server-names.conf
600 root:root /root/freenginx-endpoints.txt
--with-compat
--with-pcre
--with-pcre-jit
--with-http_ssl_module
--with-http_v2_module
--with-http_realip_module
--with-http_stub_status_module
--with-http_gzip_static_module
--with-http_sub_module
The standard HTTP modules (proxy, FastCGI, uWSGI, SCGI, gRPC, rewrite, map, gzip, SSI, access, auth_basic, limit_req, limit_conn, upstream load balancing and the rest) are built in as usual. Not included: file AIO and thread pools (see the security notes), HTTP/3, the mail and stream (TCP/UDP) proxies, WebDAV, mp4/flv streaming, slice, perl, xslt, image_filter and geoip. freenginx loads no dynamic modules on this image.
Step 10: Logs and updates
freenginx writes /var/log/nginx/access.log and /var/log/nginx/error.log, rotated daily with 14 days kept (/etc/logrotate.d/nginx). The client address in the access log is the real connection peer, followed by the host name that was requested; the stub status counters answer on 127.0.0.1:8081/nginx_status from the instance only:
sudo tail -n 2 /var/log/nginx/access.log
curl -s http://127.0.0.1:8081/nginx_status | sed -n 1p
apt-cache policy nginx | sed -n '1,3p'
127.0.0.1 - - [27/Oct/2026:09:51:23 +0000] "GET /backend/echo-headers HTTP/2.0" 200 121 "-" "curl/8.5.0" host="3.95.26.87" rt=0.001
127.0.0.1 - - [27/Oct/2026:09:51:23 +0000] "GET /latest/meta-data/ HTTP/2.0" 444 0 "-" "curl/8.5.0" host="169.254.169.254" rt=0.000
Active connections: 1
nginx:
Installed: (none)
Candidate: (none)
Candidate: (none) is the apt pin at work: Ubuntu's nginx packages would overwrite /usr/sbin/nginx, so apt never installs them here. Unattended security upgrades keep Ubuntu itself, OpenSSL, PCRE2 and zlib patched; freenginx itself is updated by launching a newer version of this listing, which cloudimg publishes when freenginx releases one.
Step 11: Security recommendations
- Keep SSH open only to your management addresses. Open 80 and 443 to the clients freenginx serves.
- Put your backend servers where only this instance can reach them, so clients cannot bypass freenginx.
- Use your own DNS name and a CA certificate (Step 7) for a public service, then consider HSTS.
- Keep the proxy snippet in every proxied location, and only add
set_real_ip_fromfor proxies you control. - Leave unattended security upgrades on.
Support and licensing
freenginx is open-source software by the freenginx project, distributed under the 2-clause BSD licence; the licence, the change log and cloudimg's provenance record (source URL, checksum, signature and build options) are in /usr/share/doc/freenginx/. cloudimg provides the build, the secure defaults, the per-instance certificate, the host pinning, the metadata block, the fail-closed start, the self-test and helpers, and 24/7 support.
cloudimg is not affiliated with or endorsed by the freenginx project or by F5, Inc. (owner of the NGINX trademark). The names are used here only to identify the software.
For help with deployment or configuration, email support@cloudimg.co.uk or visit cloudimg support.