Deployment guide

addy.io 1.7.3

Overview

addy.io (formerly AnonAddy) is an open-source email forwarding service. You give every website, shop and newsletter its own address at your domain, such as shop@admin.example.com or a random x7k2pq9a@example.com. Mail sent to an alias is forwarded to your real mailbox, you can reply through the alias without revealing your address, and an alias that starts receiving spam is switched off with one click. Self-hosting keeps the record of who mails you on a server you control.

This guide covers the addy.io 1.7.3 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud identity, secrets and PKI listing on AWS Marketplace, and you select it as the software version when you subscribe and launch. The image turns the upstream release into a closed, ready-to-use appliance.

What you get:

  • HTTPS from the first boot. nginx serves addy.io on port 443 with a certificate generated on your instance's first boot (an end-entity certificate for this instance only, never a certificate authority), and port 80 only redirects to HTTPS.
  • No default login, registration closed. Self-hosted addy.io ships with public registration switched on and has no concept of an owner, so on a public address anyone could sign up and issue aliases on your domain. This image closes registration before the configuration is first written. It then creates exactly one administrator with a password generated on the first boot, while both public listeners are still shut.
  • Never an open relay. Postfix on port 25 accepts mail only for your alias domains and relays only for the appliance itself. There is no SMTP AUTH and no submission port.
  • Your own signing keys. Each instance generates its own DKIM key (Rspamd signs forwarded mail with it), its own OpenPGP signing key and its own application secrets. addy-set-domain prints the complete MX, SPF, DKIM and DMARC records for your domain.
  • No database password at all. addy.io reaches MariaDB over the local socket as its own operating system user (MariaDB unix_socket authentication), so there is no database password to leak or rotate. MariaDB and Redis listen on loopback only, and Redis uses a per-instance password.
  • Fail closed. nginx and Postfix refuse to start until first boot has generated every secret. A guard also refuses to let either serve with an empty, reused or published default value, or with registration open.
  • Locked-down serving. Only the Laravel front controller executes, and requests for host names the instance does not serve are dropped. The client address comes from the connection (never a forwarded header), every service is blocked from the instance metadata service, and the AMI requires IMDSv2.
  • The official release, verified, built on a fully patched Ubuntu 24.04 LTS with unattended security upgrades switched on.

What is on the image: addy.io 1.7.3 (the official release in /var/www/addy-io, the complete source in /usr/share/addy-io/source), PHP 8.3 FPM, MariaDB 10.11, Redis 7.0, Postfix 3.8, Rspamd 4.2, nginx 1.24, supervisor, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.

Default user: ubuntu; Architecture: x86_64; Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet.
  • A security group that allows TCP 25 (inbound mail for your aliases) from anywhere. It must also allow TCP 443 (the web interface) and TCP 80 (redirects to HTTPS and certificate challenges) from the networks you use, and TCP 22 (SSH) from your own networks only.
  • A domain you control, for the aliases. You can explore the appliance without one, because it starts on a temporary domain that can never receive real mail.
  • An Elastic IP, so the mail server keeps its address across a stop and start.
1

Launch the instance

In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Select addy.io 1.7.3 on Ubuntu 24.04 as the software version, choose your Region, and select Launch through EC2. Choose m5.large, your key pair and a security group as described in the prerequisites. The root volume needs at least 16 GiB.

To launch with the AWS CLI instead, replace the values in angle brackets:

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=addy-io}]'

Next, allocate an Elastic IP and associate it with the instance (EC2 > Elastic IPs > Allocate, then Associate).

2

Connect to your instance

Version SSH user

addy.io 1.7.3 on Ubuntu 24.04 ubuntu

ssh -i <your-key.pem> ubuntu@<public-ip>
3

Check the services

First boot takes about half a minute. It creates the database, generates every secret and key, creates the administrator, and only then opens the public listeners. Confirm that everything is running:

sudo systemctl is-active addy-io-firstboot nginx php8.3-fpm mariadb redis-server rspamd supervisor postfix@-

Expected output:

active active active active active active active active

The only public listeners are SSH (22), SMTP (25), HTTP (80) and HTTPS (443). MariaDB, Redis and Rspamd listen on loopback only:

sudo ss -lntH | awk '{print $4}' | sort -u

Expected output:

0.0.0.0:22 0.0.0.0:25 0.0.0.0:443 0.0.0.0:80 127.0.0.1:11332 127.0.0.1:11333 127.0.0.1:11334 127.0.0.1:3306 127.0.0.1:6379 127.0.0.53%lo:53 127.0.0.54:53 [::1]:6379 [::]:22 [::]:443 [::]:80
4

Read your per-instance credentials

sudo cat /root/addy-io-credentials.txt

Expected output (your values differ; nothing in this file is shared with any other instance):

# addy.io 1.7.3 - generated on first boot by addy-io-firstboot.service
# These credentials are unique to this instance; none was baked into the image. Keep this file secret. ADDY_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com ADDY_ADMIN_USERNAME=admin ADDY_ADMIN_PASSWORD=<ADDY_ADMIN_PASSWORD> ADDY_ADMIN_RECIPIENT=admin@appliance.example.com ADDY_EMAIL_DOMAIN=addy-appliance.example.com ADDY_ALIAS_DOMAIN=admin.addy-appliance.example.com
# This instance's own signing keys (private halves stay on this instance): ADDY_DKIM_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... ADDY_GPG_SIGNING_KEY_FINGERPRINT=<40 hex characters> # addy.io reaches MariaDB over the local socket as the OS user addy (MariaDB unix_socket authentication): the # database account has NO password, so there is none to keep or rotate. On-box maintenance:
sudo mariadb MARIADB_DATABASE=addy_database MARIADB_USER=addy
# Redis (127.0.0.1:6379) password, also in /var/www/addy-io/.env (REDIS_PASSWORD): ADDY_REDIS_PASSWORD=<32 characters>
# The web interface is HTTPS only (self-signed certificate minted on this instance; port 80 redirects to it).
# Point the appliance at your own domain:
sudo addy-set-domain <your-domain.com>
# Change only the web name:
sudo addy-io-set-hostname <name>
# EC2 blocks OUTBOUND port 25 by default: forwarded mail needs AWS's port 25 removal or a relay host (see the guide). # User guide: https://secureanycloud.com/stacks/addy-io-1-7-3/
5

Check the certificate and sign in

The certificate was generated on this instance, so your browser warns that it does not know who issued it. Do not add it to your computer's trust store. Instead, compare the fingerprint your browser shows (select the warning, then View certificate) with the one on the instance:

openssl x509 -in /etc/ssl/addy-io/addy-io.crt -noout -fingerprint -sha256

Expected output (your fingerprint differs):

sha256 Fingerprint=3A:9F:...:C1

If the fingerprints match, continue to the page, open the address in ADDY_URL and sign in as admin with the password from the credentials file. Step 11 replaces the certificate with one from a public certificate authority once your DNS name points at the instance.

After you sign in, the dashboard summarises your aliases, recipients, usernames, domains and this month's bandwidth.

Change the password after your first sign in (Settings, then Security), and turn on two-factor authentication on the same page. Once you have your own password, remove the credentials file with sudo shred -u /root/addy-io-credentials.txt.

6

Why nobody else can claim your appliance

Registration does not exist on this image, so there is nothing for an attacker to use. Confirm it:

curl -sk -o /dev/null -w '%{http_code}\n' -X POST \
  -d 'username=intruder&email=intruder@example.com&password=Intruder12345' \
  https://127.0.0.1/register

Expected output:

404

The instance has exactly one account, the administrator created on first boot:

sudo mariadb -N -B -e 'SELECT COUNT(*) FROM addy_database.users;'

Expected output:

1

A guard runs before both public listeners (nginx and Postfix) start. It refuses to let either serve if registration is open, or if any secret is empty, reused or one of the defaults published upstream:

sudo /usr/local/sbin/addy-io-preflight.sh systemctl show -p ExecStartPre --value nginx.service | grep -o addy-io-preflight systemctl show -p ExecStartPre --value postfix.service | grep -o addy-io-preflight

Expected output:

addy-io-preflight: OK addy-io-preflight addy-io-preflight
7

Create your first aliases

You can create aliases in two ways.

On the fly. Make up any address at your alias domain and hand it out. The alias appears in the interface as soon as it forwards its first email.

Explicitly. Choose Aliases, then Create Alias, and pick the domain and a format: random characters, random words, a UUID or a custom local part.

You can also create aliases over the API. The following block exchanges your password for an API key and then creates an alias. It reads the password from the credentials file, so the password never appears on a command line.

PW=$(sudo grep -m1 '^ADDY_ADMIN_PASSWORD=' /root/addy-io-credentials.txt | cut -d= -f2-)
API_KEY=$(jq -n --arg p "$PW" '{username:"admin",password:$p,device_name:"cli"}' \
  | curl -sk -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' \
  --data-binary @- https://127.0.0.1/api/auth/login | jq -r .api_key) unset PW curl -sk -X POST -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
  -H 'Accept: application/json' -d '{"domain":"admin.addy-appliance.example.com"}' \
  https://127.0.0.1/api/v1/aliases | jq '.data|{email,active}'

Expected output (your alias differs):

{ "email": "k3v8qz2m@admin.addy-appliance.example.com", "active": true }

The sign-in endpoint allows three requests per minute, so space out repeated calls. Keep the API key, because the addy.io browser extensions and mobile apps use the same key.

8

Point your own domain at the appliance

Until you complete this step, the appliance uses the temporary domain addy-appliance.example.com and cannot receive real mail. One command repoints the application, Postfix and the DKIM signing tables together. It also generates a 2048-bit DKIM key for your domain, serves the web interface under your domain with a new certificate, and prints the records to publish:

sudo addy-set-domain example.net

Output (the public IP and the DKIM key are your instance's own):

==> repointing this appliance at example.net (mail host mail.example.net, web interface https://example.net/) addy-io-set-hostname: addy.io is served at https://example.net/ ========================================================================== Publish these DNS records for example.net ========================================================================== A mail.example.net <public-ip> A example.net <public-ip> A *.example.net <public-ip> MX example.net mail.example.net (priority 10) MX *.example.net mail.example.net (priority 10) TXT example.net "v=spf1 mx -all" TXT *.example.net "v=spf1 mx -all" TXT _dmarc.example.net "v=DMARC1; p=quarantine; adkim=s" TXT default._domainkey.example.net "v=DKIM1; k=rsa; p=<this instance public key>" Use an Elastic IP so the address survives a stop/start, and ask AWS to set the reverse DNS (PTR) of that Elastic IP to mail.example.net. EC2 BLOCKS OUTBOUND PORT 25 BY DEFAULT. Forwarded mail will not leave this instance until you either ask AWS to remove the port 25 restriction (the "Request to remove email sending limitations" form) or relay through a smart host such as Amazon SES on port 587. See the user guide. ==========================================================================

Use --hostname mx1.example.net if your mail host is not mail.<domain>, and --web-host addy.example.net to serve the web interface under a different name.

Each record does the following:

  • A records point the mail host, the web interface and every username subdomain (*.example.net, so anything@admin.example.net works) at your Elastic IP.
  • MX records tell other mail servers to deliver mail for your domain and its subdomains to this instance.
  • SPF (v=spf1 mx -all) states that only your MX host sends mail for the domain. If you relay through Amazon SES (Step 10), add its include, for example "v=spf1 mx include:amazonses.com -all".
  • DKIM publishes the public half of this instance's signing key. The record is printed on one line and is longer than 255 characters. Route 53 and most DNS consoles accept it pasted as is, while some need it split into several quoted strings of up to 255 characters each.
  • DMARC (p=quarantine; adkim=s) asks receivers to quarantine mail for your domain that fails strict DKIM alignment. Start with p=none if you prefer to watch reports first, and move to p=reject once mail is flowing.

You also need two things outside the instance:

  • Reverse DNS. Ask AWS to set the PTR record of your Elastic IP to your mail host (EC2 > Elastic IPs > Actions > Update reverse DNS). Many receiving servers reject mail from an address with no matching PTR record.
  • Inbound port 25 open to the internet in the security group.

Postfix decides which subdomains it accepts by asking the addy.io database through Postfix's own socket-authenticated account. Check that a username subdomain resolves:

sudo -u postfix postmap -q admin.addy-appliance.example.com \
  mysql:/etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf

Expected output:

1
9

Recipients, the mailboxes your aliases forward to

A recipient is a real mailbox that aliases forward to. The appliance starts with the temporary recipient admin@appliance.example.com. Add your own address under Recipients, then Add Recipient. addy.io sends a verification email to the new address, which requires working outbound mail (Step 10).

Once a recipient is verified, make it the default and point aliases at it. Add its PGP public key if you want forwards encrypted to you.

10

Outbound mail and EC2's port 25 restriction

Amazon EC2 restricts outbound TCP 25 on new accounts. Inbound mail to your aliases still arrives, but the forward cannot leave. Either ask AWS to lift the restriction with the Request to remove email sending limitations form, which also asks for your reverse DNS, or relay through a smart host such as Amazon SES on port 587:

sudo postconf -e 'relayhost = [email-smtp.<region>.amazonaws.com]:587'
sudo postconf -e 'smtp_sasl_auth_enable = yes'
sudo postconf -e 'smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd'
sudo postconf -e 'smtp_sasl_security_options = noanonymous'
sudo postconf -e 'smtp_tls_security_level = encrypt'
sudo install -m 600 /dev/null /etc/postfix/sasl_passwd
sudo nano /etc/postfix/sasl_passwd
# one line: [email-smtp.<region>.amazonaws.com]:587 <relay-user>:<relay-password>
sudo postmap /etc/postfix/sasl_passwd
sudo systemctl restart postfix

Watch messages leave with sudo tail -f /var/log/mail.log. The relay settings apply to outgoing mail only. The server still refuses to relay for anyone else, which you can check at any time:

postconf -h mynetworks smtpd_relay_restrictions smtpd_sasl_auth_enable

Expected output:

127.0.0.0/8 [::1]/128 permit_mynetworks, reject_unauth_destination no

From any other host, a message to an outside domain is refused with 554 5.7.1 Relay access denied.

Public DNS blocklists are deliberately off. Spamhaus refuses queries made through cloud providers' resolvers, which would turn a spam check into lost mail. Rspamd still checks SPF, DKIM and DMARC and runs its Bayesian filter. If you subscribe to Spamhaus DQS, add your key to smtpd_recipient_restrictions in /etc/postfix/main.cf.

11

Use a trusted certificate

Once your DNS name points at the instance, replace the self-generated certificate with one from Let's Encrypt. Port 80 already serves the challenge directory /var/www/acme-challenge, and nginx and Postfix both read /etc/ssl/addy-io/addy-io.crt and .key:

sudo apt-get update && sudo apt-get install -y certbot
sudo certbot certonly --webroot -w /var/www/acme-challenge -d example.net -d mail.example.net
sudo install -m 644 /etc/letsencrypt/live/example.net/fullchain.pem /etc/ssl/addy-io/addy-io.crt
sudo install -m 600 /etc/letsencrypt/live/example.net/privkey.pem /etc/ssl/addy-io/addy-io.key
sudo systemctl reload nginx postfix

To keep renewals working, add the two install lines and the reload as a certbot deploy hook (/etc/letsencrypt/renewal-hooks/deploy/addy-io.sh).

12

The complete source is on the machine

addy.io is licensed under the GNU Affero General Public License v3 or later. The complete corresponding source for the exact version running here ships on the image, together with its checksum, the licence text and a written offer. Verify it:

ls /usr/share/addy-io/source cd /usr/share/addy-io/source && sha256sum -c addy-io-1.7.3-source.tar.gz.sha256

Expected output:

LICENSE WRITTEN-OFFER.txt addy-io-1.7.3-source.tar.gz addy-io-1.7.3-source.tar.gz.sha256 addy-io-1.7.3-source.tar.gz: OK
13

Where your data lives and how to back it up

Path Contents

/var/www/addy-io The application, its configuration (.env, readable by root and the addy service user only) and the compiled frontend

/var/lib/mysql MariaDB: your aliases, recipients, domains, rules and forwarding history

/var/lib/redis Redis: sessions, cache and the delivery queue

/var/lib/rspamd/dkim This instance's DKIM private keys

/var/lib/addy-io/gnupg This instance's OpenPGP signing key

/etc/postfix Mail server configuration

/root/addy-io-credentials.txt The credentials written at first boot

Back up the database and the keys together. Restoring one without the other leaves signed mail that cannot be verified.

sudo mariadb-dump --single-transaction addy_database | gzip > ~/addy-io-$(date +%F).sql.gz
sudo tar czf ~/addy-io-keys-$(date +%F).tar.gz /var/lib/rspamd/dkim /var/lib/addy-io/gnupg /var/www/addy-io/.env

Copy both files off the instance and keep the second one private, or take EBS snapshots of the volume.

14

Day-to-day operation

Two queue workers run under supervisor and process forwarding:

sudo supervisorctl status

Expected output:

addy-io:addy-io_00 RUNNING pid 1443, uptime 0:04:06 addy-io:addy-io_01 RUNNING pid 1444, uptime 0:04:06

Every service is blocked from the instance metadata service; for example PHP:

systemctl show -p IPAddressDeny --value php8.3-fpm

Expected output:

::ffff:169.254.169.254/128 169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32

These commands are useful for routine operation:

  • Restart the application after a configuration change: sudo systemctl restart php8.3-fpm supervisor nginx
  • Watch the mail flow: sudo tail -f /var/log/mail.log
  • Watch the application log: sudo tail -f /var/www/addy-io/storage/logs/laravel.log
  • Open the Rspamd web interface over an SSH tunnel: ssh -L 11334:127.0.0.1:11334 ubuntu@<public-ip>, then browse to http://127.0.0.1:11334/

Ubuntu security updates are applied automatically by unattended-upgrades. To update by hand, run sudo apt-get update && sudo apt-get -y dist-upgrade.

What SecureAnyCloud changed

  • Registration is closed, and one administrator is created on first boot with a per-instance password. The password is passed to the application on standard input, never on a command line, and a start guard refuses upstream's published default secrets.
  • Each instance generates its own APP_KEY, Redis password, application secrets, DKIM key, OpenPGP signing key and TLS key on first boot. None of them is in the image.
  • MariaDB has no database password (socket authentication for the addy and postfix accounts). Its database directory is created fresh on first boot from a credential-free schema, and MariaDB and Redis listen on loopback only.
  • HTTPS uses a per-instance end-entity certificate, port 80 redirects, unknown host names are dropped, forwarded headers are ignored, and the 2048-bit DKIM record is printed in full.
  • Every service unit is denied the instance metadata service, and IMDSv2 is required.
  • SSH is key-only, with no root login and no X11 forwarding. The firmware update timer (fwupd-refresh) is masked, because EC2 instances have no firmware it can update. The stock snakeoil certificate generator (ssl-cert.service) is masked too, because nothing on the image uses that certificate.
  • The image has no build tools (Composer and Node.js were used for the build and removed). PHP is the Ubuntu build that carries the October 2026 PHP fixes. ImageMagick is not installed, because addy.io does not need it.

Security notes

  • Open advisories at build time (28 October 2026): none against the PHP dependencies (composer audit of both lock files) and none against the shipped frontend. The upstream lock lists vue-template-compiler (GHSA-g3ch-rx76-35fx, moderate), which is never imported and is not in the compiled frontend. The build tools were removed.
  • Restrict TCP 22, 80 and 443 to your own address ranges in the security group. Only port 25 needs to be open to everyone.
  • Keep registration closed. The start guard refuses to run either public listener if ANONADDY_ENABLE_REGISTRATION is anything but false.

Troubleshooting

The web interface does not answer. First boot may still be running. Run test -f /var/lib/sac/data/addy-io-firstboot.done && echo done, then sudo journalctl -u addy-io-firstboot -n 50. By design, nginx does not start until first boot has finished.

nginx or Postfix refuses to start. Run sudo /usr/local/sbin/addy-io-preflight.sh to see which check failed.

Aliases receive no mail. Check that the MX record points at your mail host and that inbound port 25 is open in the security group. Confirm that the postmap check in Step 8 returns 1 for your subdomain, and read /var/log/mail.log.

Forwards are not delivered. Outbound port 25 is restricted on EC2, as described in Step 10. Check the queue with sudo postqueue -p.

Mail lands in spam. Publish the SPF, DKIM and DMARC records printed by addy-set-domain, and set reverse DNS on the Elastic IP.

Support

SecureAnyCloud provides support for this image by email at sales@secureanycloud.com. See the identity, secrets and PKI listing guide for the other versions in this listing.

Deploy on AWS

Launch addy.io 1.7.3 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

Deploy on AWS