freenginx 1.31.4
Overview
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.
This guide covers launching and verifying freenginx 1.31.4 on Ubuntu 24.04 from SecureAnyCloud on Amazon EC2. The image is one version of the SecureAnyCloud Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace, and you choose it as the software version when you subscribe and launch. The listing's guide hub describes the other VPN, proxy and remote access servers in the listing.
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 SecureAnyCloud landing page and reverse-proxies the /backend/ path to a small demo backend on 127.0.0.1:8081. You can see both jobs working before you put your own site or servers behind it.
SecureAnyCloud 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 SecureAnyCloud ships the release with the fixes.
What you get:
- 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.
- Fail-closed start. 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. SecureAnyCloud 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. SecureAnyCloud 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, so size the instance for your traffic.
Connecting to your instance
freenginx 1.31.4 on Ubuntu 24.04 runs Ubuntu 24.04 LTS. Connect with the SSH user ubuntu.
Launch the instance
Option A: AWS Marketplace console
- Open the SecureAnyCloud Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace, choose View purchase options, and then choose 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 choose Launch from EC2 Console for the full launch wizard. In Version, choose freenginx 1.31.4 on Ubuntu 24.04, and then choose your Region.
- Pick an instance type (
m5.largeis recommended, and the software charge is $0.04 per vCPU per hour) and your key pair. - Select a 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), and leave the metadata defaults because the image already requires IMDSv2. Then choose Launch.
Option B: AWS CLI
Run this command on your own workstation. It creates a security group that allows SSH from your management address and the web ports from everyone, and 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.
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/sac/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://secureanycloud.com/stacks/freenginx-1-31-4/Confirm freenginx is running
Confirm that freenginx is active and first boot has completed. freenginx 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 || trueactive active nginx version: freenginx/1.31.4 0.0.0.0:443 0.0.0.0:80 127.0.0.1:8081 [::]:443 [::]:80 0Run 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-selftestok: 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_OKSend traffic through freenginx
A request for the instance's name over HTTPS returns the landing page over HTTP/2 with Server: freenginx (no version). A request for /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 'SAC_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/ || trueHTTP/2 200 server: freenginx x-content-type-options: nosniff <title>freenginx is serving - sac</title> SAC_FREENGINX_BACKEND 301 https://3.95.26.87/docs 000The 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.87Open https://<public-ip>/ in your own browser. After you accept the self-signed certificate, you see the landing page, and its /backend/ link shows the demo backend reached through freenginx.
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-sac-default-server.conf holds the catch-all servers that drop unknown host names, and reusable pieces are in snippets/:
ls /etc/nginx /etc/nginx/conf.d /etc/nginx/sites-enabled/etc/nginx: sac 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-sac-default-server.conf /etc/nginx/sites-enabled: defaultStatic 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. The headers that 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/sac-proxy-headers.conflocation /backend/ { proxy_pass http://127.0.0.1:8081/; include /etc/nginx/snippets/sac-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-pagerAllow traffic from this instance's security group to your servers, and stop exposing them directly. If another proxy sits 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.
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, which is useful after attaching an Elastic IP. With arguments, it uses the names you give, for example the DNS name that 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 -1The names are written to two small include files:
sudo cat /etc/nginx/sac/server-names.conf /etc/nginx/sac/https-redirect.conf# sac: 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;
# sac: 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/sac.crt and its key to /etc/nginx/ssl/sac.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.
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 in Step 4 proved 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.service000 IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128Even 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.
How the fail-closed start works, and what is compiled in
freenginx starts only once first boot has written /var/lib/sac/data/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/sac.key /etc/nginx/sac/server-names.conf /root/freenginx-endpoints.txt nginx -V 2>&1 | tr ' ' '\n' | grep -E '^--with-(http|pcre|compat)'ConditionPathExists=/var/lib/sac/data/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/sac.key 644 root:root /etc/nginx/sac/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_moduleThe 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.
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. To update freenginx itself, launch a newer version of this listing, which SecureAnyCloud publishes when freenginx releases one.
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 SecureAnyCloud's provenance record (source URL, checksum, signature and build options) are in /usr/share/doc/freenginx/. SecureAnyCloud 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.
SecureAnyCloud 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 sales@secureanycloud.com or visit SecureAnyCloud support.
Deploy on AWS
Launch freenginx 1.31.4 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com