Security Azure

addy.io on Ubuntu 24.04 on Azure User Guide

| Product: addy.io on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of addy.io on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. addy.io (formerly AnonAddy) is a self hosted email aliasing service: instead of handing your real address to every shop, forum, newsletter and app that asks for one, you give each of them a different alias on your own domain.

Mail sent to an alias is forwarded to your real inbox, and you can reply from the alias so the other side never learns where it actually lands. When an alias starts attracting spam you switch it off or delete it, and only that one correspondent is affected. Because every alias is unique to one recipient, an address that suddenly starts receiving spam tells you exactly who leaked or sold it.

Aliases can be created from the web interface, from the official browser extensions and mobile apps, or through the API. Each one carries a description so you remember what it was for, can be enabled and disabled at any time, and keeps a count of what has been forwarded, blocked and replied through it. You can add several recipient mailboxes and point different aliases at different ones, block individual senders, build rules that act on incoming mail, and have forwards signed and encrypted with your own PGP key.

The cloudimg image ships the free and open source, AGPL-3.0 licensed addy.io 1.7.0, run unmodified from the upstream release tag. The Laravel application runs under PHP 8.3-FPM with MariaDB and Redis on the loopback interface only; nginx on ports 80 and 443 is the single public web listener and Postfix on port 25 is the single public mail listener. Rspamd signs your outgoing alias mail with DKIM and ARC and checks SPF, DKIM and DMARC on the way in. Backed by 24/7 cloudimg support.

addy.io is an independent open source project. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by addy.io. It ships the free and open source AGPL-3.0 licensed software, unmodified.

The nginx, postfix, php8.3-fpm, mariadb, redis-server, rspamd and supervisor services all active, two Laravel queue workers RUNNING, and only ports 22, 25, 80 and 443 listening publicly

What is included:

  • addy.io 1.7.0 (AGPL-3.0), unmodified upstream, with the complete corresponding source on the image
  • PHP 8.3-FPM, MariaDB 10.11, Redis 7.0, Postfix 3.8, Rspamd and nginx, all from the Ubuntu 24.04 archive so they keep receiving distribution security updates
  • A first boot service that generates every secret on your own VM and seeds exactly one administrator
  • A fail closed guard that refuses to open either public listener with an empty, unrotated or published default secret
  • addy-set-domain, a single command that repoints the whole appliance at your domain and prints the DNS records to publish

Prerequisites

  • An active Azure subscription
  • Permission to create virtual machines and network security rules
  • An SSH key pair for administrative access
  • A domain you control, with access to its DNS. Aliases cannot receive real mail until your domain's MX record points at this VM.
  • Inbound TCP 80 and 443 from the networks that will use the web interface
  • Inbound TCP 25 from anywhere, so other mail servers can deliver to your aliases
  • The recommended size is Standard_B2s (2 vCPU, 4 GiB) or larger

Step 1: Deploy from the Azure Portal

  1. Sign in to the Azure Portal and choose Create a resource.
  2. Search the Marketplace for addy.io by cloudimg and select the offer.
  3. Choose your subscription, resource group and region.
  4. Set the VM size to Standard_B2s or larger.
  5. Set the administrator username to azureuser and upload or generate an SSH public key.
  6. On Disks, choose Standard SSD or Premium SSD.
  7. On Networking, allow inbound SSH (22), HTTP (80), HTTPS (443) and SMTP (25). Restrict 22, 80 and 443 to your own address ranges; 25 must be open to the internet for mail delivery to work.
  8. Review and create. The VM boots, generates its own secrets, seeds its administrator and only then opens the public listeners.

Step 2: Deploy from the Azure CLI

Replace the placeholders with your own values.

az group create \
  --name my-resource-group \
  --location eastus

az vm create \
  --resource-group my-resource-group \
  --name addy-io-01 \
  --image cloudimg:addy-io-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --storage-sku StandardSSD_LRS \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

# open the ports the appliance needs
az vm open-port --resource-group my-resource-group --name addy-io-01 --port 80 --priority 1001
az vm open-port --resource-group my-resource-group --name addy-io-01 --port 443 --priority 1002
az vm open-port --resource-group my-resource-group --name addy-io-01 --port 25 --priority 1003

Azure blocks outbound port 25 on most subscriptions. That does not stop aliases receiving mail, but it does stop the appliance delivering forwards directly. See Step 11 for the relay configuration.

Step 3: Connect to your VM

ssh azureuser@<vm-ip>

Step 4: Confirm the appliance is running

First boot generates every secret, rotates the database password, creates this VM's own DKIM key, seeds the administrator and only then opens the public listeners. It normally completes within two minutes of the VM becoming reachable.

Check that first boot finished:

test -f /var/lib/cloudimg/addy-io-firstboot.done && echo "first boot completed"

Expected output:

first boot completed

Check the services:

for s in nginx postfix php8.3-fpm mariadb redis-server rspamd supervisor; do
  printf '%-16s %s\n' "$s" "$(systemctl is-active $s)"
done

Expected output:

nginx            active
postfix          active
php8.3-fpm       active
mariadb          active
redis-server     active
rspamd           active
supervisor       active

Check the queue workers that deliver forwarded mail:

sudo supervisorctl status 'addy-io:*'

Expected output:

addy-io:addy-io_00               RUNNING   pid 46779, uptime 0:09:55
addy-io:addy-io_01               RUNNING   pid 46780, uptime 0:09:55

Confirm the web interface answers:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/login

Expected output:

200

Only four ports are reachable from outside the VM. Everything else is bound to loopback:

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

Step 5: Read your per instance credentials

Every secret on this appliance is generated on your own virtual machine at first boot and exists nowhere else. There is no default password, and no cloudimg image ever ships one.

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

Expected output (your values will differ):

# addy.io - per-VM credentials, generated on this machine at first boot.
# Nothing here is shared with any other virtual machine or with cloudimg.
ADDY_URL=http://20.30.40.50
ADDY_ADMIN_USERNAME=admin
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
ADDY_DKIM_PUBLIC_KEY=<public key, publish as a DNS TXT record>

The file is readable only by root:

sudo stat -c '%a %U:%G' /root/addy-io-credentials.txt

Expected output:

600 root:root

The first boot sentinel present, the per instance credentials file with its generated administrator password, and the file readable only by root

ADDY_EMAIL_DOMAIN is a placeholder — addy-appliance.example.com is a subdomain of the RFC 2606 documentation domain, so no third party can ever own it and an unconfigured appliance can never emit mail at a real party. Step 9 replaces it with your own domain.

Step 6: Sign in

Browse to the address in ADDY_URL and sign in with the username and password from the credentials file.

The addy.io sign in page, asking for a username and password

After signing in you land on the dashboard, which summarises your aliases, recipients, usernames, domains and this month's bandwidth.

The addy.io dashboard showing one recipient, one username and one alias, with the monthly bandwidth allowance

Change the password after your first sign in — go to Settings, then Change password. Once you have set your own password you can delete the credentials file:

sudo shred -u /root/addy-io-credentials.txt

Step 7: Why nobody else can claim your appliance

Self hosted addy.io ships with ANONADDY_ENABLE_REGISTRATION=true and has no concept of an owner. On a public address that means any passer by can register an account on your server and start issuing aliases on your domain. Upstream's own instructions tell you to create your account first and disable registration afterwards, which leaves a window open.

This image never enters that state. Registration is closed in the configuration from the moment it is written, and both public listeners are held shut until exactly one administrator exists.

Confirm registration is refused:

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

Expected output — the route does not exist at all, so there is nothing to attack:

404

Confirm there is exactly one account:

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

Expected output:

1

Postfix answering on port 25, the alias domain resolving through the application database, an anonymous registration refused with HTTP 404, and exactly one account on the appliance

A guard runs before both public listeners start and refuses to let either serve if any secret is empty, still unrotated, or one of the values published in upstream's .env.example:

sudo /usr/local/sbin/addy-io-preflight.sh

Expected output:

addy-io-preflight: OK

You can see it wired in front of each listener:

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
addy-io-preflight

Step 8: Create your first aliases

There are two ways to create aliases.

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. Nothing needs to be created in advance.

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

The aliases list showing a live alias on the appliance's own domain, with its recipient, forward and reply counters and an active toggle

Aliases can also be created over the API. First exchange your password for an API key, then call the alias endpoint:

API_KEY=$(curl -s -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"username":"admin","password":"<your-password>","device_name":"cli"}' \
  http://127.0.0.1/api/auth/login | jq -r .api_key)

curl -s -X POST -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"domain":"admin.addy-appliance.example.com"}' \
  http://127.0.0.1/api/v1/aliases | jq '.data|{email,active,created_at}'

Expected output (your alias will differ):

{
  "email": "vz1gmyww@admin.addy-appliance.example.com",
  "active": true,
  "created_at": "2026-07-26 07:17:18"
}

Creating an alias over the API and listing the aliases on the appliance back

The sign in endpoint is rate limited to three requests per minute, so space out repeated calls. Once you have an API key, save it — the browser extensions and the official mobile apps use the same key.

Step 9: Point your own domain at the appliance

Until you do this, the appliance uses the placeholder domain and cannot receive real mail. One command repoints the application, the mail server and the DKIM signing tables together:

sudo addy-set-domain example.com

It rewrites the application configuration, generates a 2048-bit DKIM key for your domain, re-renders Postfix and the Rspamd signing tables, reloads every service, and prints exactly what to publish:

==========================================================================
 Publish these DNS records for example.com
==========================================================================
  A      mail.example.com          20.30.40.50
  A      example.com               20.30.40.50
  A      *.example.com             20.30.40.50
  MX     example.com               mail.example.com   (priority 10)
  MX     *.example.com             mail.example.com   (priority 10)
  TXT    example.com               "v=spf1 mx ~all"
  TXT    *.example.com             "v=spf1 mx ~all"
  TXT    _dmarc.example.com        "v=DMARC1; p=none; sp=none; adkim=r; aspf=r; pct=100;"
  TXT    default._domainkey.example.com
         "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A..."
==========================================================================

Pass --hostname if your mail host is not mail.<domain>, and --https once you have a certificate. Run this form instead of the one above, not as well as it:

sudo addy-set-domain example.com --hostname mx1.example.com --https

Two more things are needed outside the VM:

  1. Reverse DNS. In the Azure Portal, open the VM's public IP address, set the DNS name label and then set reverse DNS to your mail hostname. Many receiving mail servers reject mail from an address with no matching PTR record.
  2. Inbound port 25. Make sure the network security group allows TCP 25 from anywhere.

Confirm Postfix now accepts mail for your alias domain — this is a live lookup through the application database, not just a file check. Query the username subdomain form, which is what the database map resolves (the apex domain is accepted separately, through virtual_mailbox_domains):

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

Expected output:

1

Step 10: Recipients, the mailboxes your aliases forward to

A recipient is a real mailbox that aliases forward to. The appliance starts with one placeholder recipient; replace it with your own address.

Choose Recipients, then Add Recipient, and enter your real email address. addy.io sends a verification email to it — which requires outbound mail to be working, so complete Step 11 first if Azure is blocking port 25.

The recipients page, where the destination mailbox for forwarded mail is managed and verified

Once a recipient is verified you can point individual aliases at it, add a PGP public key so forwards are encrypted, and make it the default for new aliases.

Step 11: Outbound mail and Azure's port 25 block

Azure blocks outbound TCP 25 on most subscriptions. Inbound mail to your aliases still arrives, but the appliance cannot deliver the forward. The fix is to relay through a smart host.

Add your provider's details to Postfix:

sudo postconf -e 'relayhost = [smtp.example-relay.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'

echo '[smtp.example-relay.com]:587 <relay-user>:<relay-password>' \
  | sudo tee /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
sudo systemctl restart postfix

Then add the relay to your SPF record so receivers accept mail sent on your behalf, for example:

TXT  example.com  "v=spf1 mx include:spf.example-relay.com ~all"

Watch a message go out:

sudo tail -f /var/log/mail.log

Public DNS blocklists are deliberately off. Upstream's reference configuration enables Spamhaus zen and dbl lookups. Those return "query refused" for every query made through a public cloud provider's resolver, which turns a spam check into a mail losing rejection. Subscribe to Spamhaus DQS (free for personal use) and uncomment the pre-written lines in /etc/postfix/main.cf with your own key if you want them. Rspamd still performs SPF, DKIM, DMARC and Bayesian checks either way.

Step 12: Where your data lives

Path Contents
/var/www/addy-io The application, its configuration (.env) 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 VM's own DKIM private keys
/etc/postfix Mail server configuration and the database lookup maps
/root/addy-io-credentials.txt The per instance credentials written at first boot
/usr/share/addy-io/source The complete corresponding AGPL source, its checksum and the written offer

Back up the database and the DKIM keys together — restoring one without the other leaves signed mail that cannot be verified:

sudo mariadb-dump -uroot --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/www/addy-io/.env

Step 13: The complete source is on the machine

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

ls /usr/share/addy-io/source

Expected output:

LICENSE
WRITTEN-OFFER.txt
addy-io-1.7.0-source.tar.gz
addy-io-1.7.0-source.tar.gz.sha256

Verify it:

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

Expected output:

addy-io-1.7.0-source.tar.gz: OK

Step 14: Add HTTPS

The appliance answers on 443 with a self signed certificate from first boot, so the port is open and working immediately, but browsers will warn. Once your DNS A record points at the VM, replace it with a real certificate:

sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d '*.example.com'
sudo addy-set-domain example.com --https

The --https flag switches the application URL to https:// and turns on secure session cookies. Certbot installs a renewal timer automatically.

Step 15: Day to day 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

Check the Rspamd view of a message's spam score, over an SSH tunnel from your own machine:

ssh -L 11334:127.0.0.1:11334 azureuser@<vm-ip>

Then open http://127.0.0.1:11334/ in your browser.

Ubuntu security updates are applied automatically by unattended-upgrades. To update the operating system by hand:

sudo apt-get update && sudo apt-get -y dist-upgrade

Security hardening

  • Change the administrator password at first sign in, then shred the credentials file
  • Enable two factor authentication under Settings, then Two factor authentication
  • Restrict inbound 22, 80 and 443 to your own address ranges in the network security group; only 25 needs to be open to the internet
  • Add a real TLS certificate (Step 14) before using the interface over the internet
  • Keep registration closed. The guard refuses to start either public listener if ANONADDY_ENABLE_REGISTRATION is missing or true
  • Add a PGP public key to your recipient so forwarded mail is encrypted at rest in your mailbox
  • Set p=quarantine or p=reject in your DMARC record once you have confirmed your mail is being delivered and signed correctly

Troubleshooting

The web interface does not answer. First boot may still be running. Check test -f /var/lib/cloudimg/addy-io-firstboot.done, then sudo journalctl -u addy-io-firstboot -n 50. nginx will not start at all until first boot has seeded the administrator — that is deliberate.

nginx or Postfix refuse to start. Run sudo /usr/local/sbin/addy-io-preflight.sh to see which check failed. The guard blocks an empty, unrotated or published default secret.

Aliases are not receiving mail. Confirm your MX record points at the VM's hostname, that inbound 25 is open in the network security group, and that sudo postmap -q <your-domain> mysql:/etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf returns 1. Then check /var/log/mail.log.

Forwards are not being delivered. Azure is almost certainly blocking outbound port 25 — see Step 11. Check the queue with sudo postqueue -p and the workers with sudo supervisorctl status 'addy-io:*'.

Mail is being marked as spam. Publish the SPF, DKIM and DMARC records printed by addy-set-domain, and set reverse DNS for the VM's public IP to your mail hostname.

A recipient never receives its verification email. Outbound mail is not working yet; complete the relay configuration in Step 11.

Support

cloudimg provides 24/7 support for this image. Contact support@cloudimg.co.uk.