Ferron 2.8.1 on AWS User Guide
Overview
This guide covers launching and verifying Ferron 2.8.1 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.
Ferron is an open-source web server and reverse proxy written in Rust. It serves static sites, reverse-proxies and load-balances HTTP and HTTPS traffic to backend servers, speaks HTTP/1.1 and HTTP/2, and is configured from one readable KDL file.
On this image Ferron 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.
Ferron 2.8.1 is the newest stable release. cloudimg ships the official upstream release binaries unmodified (each one checksum-pinned); the release, licence and checksums are recorded in /usr/share/doc/ferron/PROVENANCE.txt.
What is on the image:
- Ferron 2.8.1 (
ferron.service) running as the unprivilegedferronuser, allowed only to bind ports 80 and 443 - HTTPS on 443 with a self-signed certificate generated on your instance's first boot; port 80 only redirects to HTTPS
- A landing page in
/var/www/ferronand a demo backend (ferron-backend.service, a second Ferron) on127.0.0.1:8081, the target of/backend/ - Helpers:
ferron-selftestandferron-tls-cert(certificate and served names) - No metadata access: neither Ferron service can 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. Ferron 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 0640, ownerroot, groupferron). No key or certificate is baked into the image. - Only your names are served. Ferron answers only the names first boot read from the instance metadata (public IPv4 address, public DNS name, private IPv4 address) and
localhost. A request for any other host name gets421 Misdirected Requeston ports 80 and 443 and never reaches a backend. - Client addresses come from the connection, never from headers. Ferron rebuilds
X-Forwarded-For,X-Forwarded-ProtoandForwardedfrom the real connection, this image replacesX-Real-IPwith the real peer address (upstream Ferron passes a client'sX-Real-IPthrough), and removes other client-address headers such asTrue-Client-IPbefore a request reaches your backend. - No forward proxy, no outbound calls. Ferron's forward-proxy feature is off, so a request for another site (
GET http://other-host/...orCONNECT) is refused with501. Automatic certificates (ACME) and OCSP stapling are off, so Ferron makes no connection of its own. - No SSRF to instance credentials.
ferron.serviceandferron-backend.servicecarry 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. Ferron's 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. - TLS 1.2 and 1.3 with AEAD ciphers only (Ferron's rustls TLS stack), no
Serverheader, directory listings off andX-Content-Type-Options: nosniff. - Least privilege. Ferron runs as the
ferronsystem user with one capability (binding ports below 1024), a read-only system and a private/tmp; the demo backend runs with no capability at all. - Fails closed. Ferron 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 first. If first boot cannot finish, Ferron 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 Ferron 2.8.1. Ferron 2.8.1 bundles third-party Rust libraries that have published advisories with no Ferron 2.x release that updates them yet. cloudimg checked each one against the shipped configuration: the high-severity ones affect only HTTP/3 (the quinn-proto QUIC library), automatic certificates through a DNS provider (quick-xml, hickory-resolver) and DNS SRV backends, none of which this image enables. HTTP/3 is not offered on this image: Ferron's start-up check refuses protocols ... "h3". Two lower-severity advisories apply to normal HTTPS traffic (h2 empty DATA frames, rated low; rustls handshake message handling, rated medium). The GitHub security advisories published for Ferron itself affect the 3.0 release candidates only, not 2.8.1. cloudimg ships an updated version of this listing when Ferron publishes one.
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 Ferron to serve
Recommended instance type: m5.large. Ferron is light; size the instance for your traffic.
Connecting to your instance
| Version | Operating system | SSH user |
|---|---|---|
| Ferron 2.8.1 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 Ferron 2.8.1 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 ferron \
--description "Ferron 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=ferron}]'
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 Ferron serves, and starts the demo backend and Ferron. The details are in /root/ferron-endpoints.txt, readable only by root. Nothing in it is a secret:
sudo cat /root/ferron-endpoints.txt
# Ferron 2.8.1 - generated on first boot by ferron-firstboot.service. Nothing here is a secret.
# The TLS key in /etc/ferron/tls was generated on this instance; none was baked into the image. Ferron has no
# login of its own: the landing page and /backend/ are public web content by design.
FERRON_HOST=54.172.20.227
FERRON_URL=https://54.172.20.227/
FERRON_NAMES=54.172.20.227 ec2-54-172-20-227.compute-1.amazonaws.com 172.31.91.248 localhost 127.0.0.1
TLS_CERT_SHA256=A6:B5:22:98:A4:A7:8C:A8:88:3C:D2:90:1E:07:4F:8A:9E:7E:E7:CA:5D:2E:9F:06:4B:C7:C0:03:57:ED:12:F8
FERRON_CONFIG=/etc/ferron/ferron.kdl
FERRON_HOSTS=/etc/ferron/cloudimg-hosts.kdl
FERRON_WEBROOT=/var/www/ferron
# Open TCP 443 (and 80, which only redirects) in the security group to the clients that should reach Ferron.
# Your site: /var/www/ferron (or 'root' in /etc/ferron/ferron.kdl), then sudo systemctl reload ferron
# Your backend: the 'proxy' line under location "/backend" in /etc/ferron/ferron.kdl
# New name or Elastic IP: sudo ferron-tls-cert <name-or-ip> ...
# Self-test: sudo ferron-selftest
# Guide: https://www.cloudimg.co.uk/guides/ferron-2-8-1-on-ubuntu-24-04-aws/
Step 3: Confirm Ferron is running
Ferron and the demo backend are active and first boot has completed. Ferron listens on ports 80 and 443 on every interface; the demo backend listens on 127.0.0.1 only, and nothing listens on UDP (HTTP/3 is off):
systemctl is-active ferron.service ferron-backend.service ferron-firstboot.service
ferron -V | head -1
sudo ss -Htlnp '( sport = :80 or sport = :443 or sport = :8081 )' | awk '{print $4}' | sort -u
sudo ss -Hulnp | grep -c ferron || true
active
active
active
Ferron 2.8.1
*:443
*:80
127.0.0.1:8081
0
Step 4: Run the self-test
The shipped self-test checks the whole path from the instance itself: the landing page is served over HTTPS for the instance's name and /backend/ traverses Ferron 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 refused, forward-proxy requests are refused, the demo backend is refused on the private address, and the instance metadata service is unreachable from inside both Ferron services:
sudo ferron-selftest
ok: ferron and ferron-backend are active
ok: https://54.172.20.227/ serves the landing page and /backend/ traverses Ferron to the loopback-only demo backend
ok: the certificate served on 443 is this instance's (A6:B5:22:98:A4:A7:8C:A8...)
ok: port 80 redirects to https://54.172.20.227/; an unknown Host is refused (421) on 80 and 443
ok: forward-proxy requests are refused (HTTP/1.1 501 Not Implemented)
ok: the demo backend 8081 is refused on 172.31.91.248
ok: metadata: 169.254.169.254 is unreachable from inside ferron and ferron-backend
SELFTEST_OK
Step 5: Send traffic through Ferron
A request for the instance's name over HTTPS returns the landing page with Ferron's X-Served-By: ferron header and no Server header; /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 is refused with HTTP 421:
H=$(sudo sed -n 's/^FERRON_HOST=//p' /root/ferron-endpoints.txt)
curl -sk -D - -o /tmp/ferron-page.html --resolve "$H:443:127.0.0.1" "https://$H/" | grep -iE '^(HTTP|x-served-by|server)'
grep -o '<title>[^<]*</title>' /tmp/ferron-page.html; rm -f /tmp/ferron-page.html
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/backend/" | grep -o 'CLOUDIMG_FERRON_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/
HTTP/2 200
x-served-by: ferron
<title>Ferron is serving - cloudimg</title>
CLOUDIMG_FERRON_BACKEND
301 https://54.172.20.227/docs
421
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 Ferron.
Step 6: Serve your own site or point the proxy at your servers
The configuration is /etc/ferron/ferron.kdl. Static files come from root "/var/www/ferron" in the :443 block, so the simplest change is to put your site's files in /var/www/ferron. The reverse proxy is the location "/backend" block:
sudo sed -n '/location "\/backend"/,/^ }/p' /etc/ferron/ferron.kdl
location "/backend" remove_base=#true {
if_not "CLOUDIMG_PINNED_HOST" {
status 421
}
proxy "http://127.0.0.1:8081/"
// Client IP from the socket: Ferron rebuilds X-Forwarded-For / X-Forwarded-Proto / Forwarded from the
// connection; X-Real-IP is replaced and other client-IP headers are removed.
proxy_request_header_replace "X-Real-IP" "{client_ip_canonical}"
proxy_request_header_remove "True-Client-IP"
proxy_request_header_remove "X-Client-IP"
proxy_request_header_remove "Client-IP"
proxy_request_header_remove "CF-Connecting-IP"
proxy_request_header_remove "X-Forwarded-Port"
proxy_request_header_remove "X-Forwarded-Server"
proxy_request_header_remove "X-Original-Forwarded-For"
}
Change the proxy line to your own server (add more proxy lines to load-balance between several), change the location path, or move the proxy line out of the location to proxy the whole site. Keep the if_not "CLOUDIMG_PINNED_HOST" lines in every block and location you add, so unknown host names stay refused. Then reload; Ferron re-reads the file, and the reload is refused if the start-up check fails:
sudo nano /etc/ferron/ferron.kdl
sudo systemctl reload ferron
sudo journalctl -u ferron -n 20 --no-pager
Allow traffic from this instance's security group to your servers, and stop exposing them directly. 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 full directive reference is in the Ferron documentation.
Step 7: Use your own domain name
ferron-tls-cert re-issues the self-signed certificate, updates the names Ferron serves and reloads Ferron. 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 ferron-tls-cert <www.example.com>
sudo ferron-selftest | tail -1
The names are written to /etc/ferron/cloudimg-hosts.kdl:
sudo cat /etc/ferron/cloudimg-hosts.kdl
// cloudimg: written by ferron-tls-cert on 2026-10-27T07:32:12Z. The names this instance serves (any other
// Host gets 421) and the https redirect target for port 80. Change it with: sudo ferron-tls-cert <name> ...
snippet "CLOUDIMG_PINNED_HOSTS" {
is_regex "{header:Host}" "^(54\\.172\\.20\\.227|ec2-54-172-20-227\\.compute-1\\.amazonaws\\.com|172\\.31\\.91\\.248|localhost|127\\.0\\.0\\.1)(:(80|443))?$" case_insensitive=#true
}
snippet "CLOUDIMG_HTTPS_REDIRECT" {
status 301 location="https://54.172.20.227{path_and_query}"
}
ferron-tls-cert always adds localhost and 127.0.0.1. To use a certificate from a public CA instead, copy its full chain to /etc/ferron/tls/cloudimg.crt and its key to /etc/ferron/tls/cloudimg.key (mode 0640, owner root, group ferron), make sure your DNS name is in cloudimg-hosts.kdl (run ferron-tls-cert with it first), then run sudo systemctl reload ferron. Ferron can also obtain certificates itself (ACME); that is off on this image because it needs a public DNS name. If you enable it, note that the metadata block means a DNS-provider challenge cannot use the instance role: supply the provider's own credentials in the configuration.
Step 8: Forward proxy and metadata access are blocked
A request for another site through Ferron is refused, and both services carry the metadata deny rule; the self-test proved in Step 4 that the metadata service does not answer from inside either of them:
H=$(sudo sed -n 's/^FERRON_HOST=//p' /root/ferron-endpoints.txt)
printf 'GET http://169.254.169.254/latest/meta-data/ HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n' "$H" | timeout 10 openssl s_client -quiet -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | head -1
systemctl show -p IPAddressDeny ferron.service ferron-backend.service | grep .
HTTP/1.1 501 Not Implemented
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32
Even if a proxy line were pointed at 169.254.169.254, Ferron's start-up check would refuse the configuration, and the deny rule would block the connection anyway.
Step 9: How the fail-closed start works
Ferron starts only once first boot has written /var/lib/cloudimg/ferron-ready. Every start and every reload runs a check (as root) that refuses to start Ferron without this instance's key, certificate and names, with HTTP/3 enabled or with a proxy target that names the metadata service, and only then Ferron as the ferron user:
systemctl cat ferron.service | grep -E '^(ConditionPathExists|ExecStartPre|User|AmbientCapabilities)='
sudo stat -c '%a %U:%G %n' /etc/ferron/tls/cloudimg.key /etc/ferron/cloudimg-hosts.kdl /root/ferron-endpoints.txt
sudo grep -E '^ (protocols|auto_tls|ocsp_stapling) ' /etc/ferron/ferron.kdl
ConditionPathExists=/var/lib/cloudimg/ferron-ready
User=ferron
ExecStartPre=+/usr/local/sbin/ferron-preflight /etc/ferron
AmbientCapabilities=CAP_NET_BIND_SERVICE
640 root:ferron /etc/ferron/tls/cloudimg.key
640 root:ferron /etc/ferron/cloudimg-hosts.kdl
600 root:root /root/ferron-endpoints.txt
protocols "h1" "h2"
auto_tls #false
ocsp_stapling #false
Step 10: Logs
Ferron writes an access log and an error log to /var/log/ferron/, rotated at 50 MB (five files kept). The client address in the access log is the real connection peer:
sudo tail -n 2 /var/log/ferron/access.log
::ffff:127.0.0.1 - - [27/Oct/2026:07:33:37 +0000] "GET / HTTP/2.0" 421 1892 "-" "curl/8.5.0"
::ffff:127.0.0.1 - - [27/Oct/2026:07:33:37 +0000] "GET /latest/meta-data/ HTTP/1.1" 501 1894 "-" "-"
Step 11: Security recommendations
- Keep SSH open only to your management addresses. Open 80 and 443 to the clients Ferron serves.
- Put your backend servers where only this instance can reach them, so clients cannot bypass Ferron.
- Use your own DNS name and a CA certificate (Step 7) for a public service.
- Keep HTTP/3 off on this release; cloudimg ships an updated version of this listing when Ferron publishes a release with updated libraries.
- Leave unattended security upgrades on. Ferron itself is the binaries in
/opt/ferron/bin; cloudimg ships updated versions as new versions of this listing.
Support and licensing
Ferron is open-source software by the Ferron project, distributed under the MIT License; the licence and cloudimg's provenance record (release URL and checksums) are in /usr/share/doc/ferron/. cloudimg provides the packaging, 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 Ferron project. Ferron is used here only to identify the software.
For help with deployment or configuration, email support@cloudimg.co.uk or visit cloudimg support.