Deployment guide

addy.io

Overview

addy.io (formerly AnonAddy) is a self-hosted email aliasing service. Instead of giving your real address to every shop, forum, newsletter and app, you give each one 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 your mail lands.

When an alias starts attracting spam, switch it off or delete it, and only that one correspondent is affected. Because every alias is unique to one recipient, an address that starts receiving spam tells you exactly who leaked or sold it.

You can create aliases from the web interface, the official browser extensions and mobile apps, or the API. Each alias carries a description, can be enabled and disabled at any time, and keeps a count of what has been forwarded, blocked and replied through it. You can also 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.

This guide covers deploying and configuring addy.io on Ubuntu 24.04 on Azure from the SecureAnyCloud Azure Marketplace image. The 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 inbound mail. The image is backed by 24/7 SecureAnyCloud support.

addy.io is an independent open source project. This image is produced by SecureAnyCloud 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.

What you get:

  • 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
1

Deploy from the Azure Portal

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

Deploy from the Azure CLI

Replace the example values with your own.

az group create \
  --name my-resource-group \
  --location eastus
az vm image terms accept --urn sac1647283583153:addy-io-ubuntu-24-04:default:latest
az vm create \
  --resource-group my-resource-group \
  --name addy-io-01 \
  --image sac1647283583153: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. This does not stop aliases from receiving mail, but it does stop the appliance from delivering forwards directly. See Step 11 for the relay configuration.

3

Connect to your VM

ssh azureuser@<vm-ip>
4

Confirm the appliance is running

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

Check that first boot finished:

test -f /var/lib/sac/data/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
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 SecureAnyCloud image 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 sac. ADDY_URL=http://20.30.40.50 ADDY_ADMIN_USERNAME=admin ADDY_ADMIN_PASSWORD=<set-at-first-boot> 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

ADDY_EMAIL_DOMAIN starts out as addy-appliance.example.com, a subdomain of the RFC 2606 documentation domain. No third party can own it, so an unconfigured appliance can never send mail as a real party. Step 9 replaces it with your own domain.

6

Sign in

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

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. Go to Settings, then Change password. After you set your own password, you can delete the credentials file:

sudo shred -u /root/addy-io-credentials.txt
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, any passerby 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. Both public listeners stay 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

A guard runs before both public listeners start. It 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

Confirm that the guard is 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
8

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, so 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.

You can also create aliases 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" }

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

9

Point your own domain at the appliance

Until you complete this step, the appliance uses the documentation domain from Step 5 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 pass --https once you have a certificate. Run this form instead of the one above, not in addition to it:

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

Two more things are needed outside the VM:

  • 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 that has no matching PTR record.
  • Inbound port 25. Make sure the network security group allows TCP 25 from anywhere.

Confirm that 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
10

Recipients, the mailboxes your aliases forward to

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

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

After 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.

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. To fix this, relay outbound mail 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 lookups return "query refused" for every query made through a public cloud provider's resolver, which turns a spam check into a rejection that can lose legitimate mail. To use them, subscribe to Spamhaus DQS (free for personal use) and uncomment the pre-written lines in /etc/postfix/main.cf with your own key. Rspamd performs SPF, DKIM, DMARC and Bayesian checks either way.

12

Where your data lives

Path: Contents

/var/www/addy-io: The application, its configuration (.env) and the compiled frontend

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

/var/lib/redis: Redis, which holds 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
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
14

Add HTTPS

The appliance answers on 443 with a self-signed certificate from first boot. The port is open and working immediately, but browsers show a warning. After your DNS A record points at the VM, replace the certificate with a real one:

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.

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/sac/data/addy-io-firstboot.done, then sudo journalctl -u addy-io-firstboot -n 50. nginx does not start until first boot has seeded the administrator. This 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 most likely 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

SecureAnyCloud provides 24/7 support for this image. Contact sales@secureanycloud.com.

Deploy on Azure

Launch addy.io on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure