Security Azure

SimpleLogin on Ubuntu 24.04 on Azure User Guide

| Product: SimpleLogin on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of SimpleLogin on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. SimpleLogin 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 note so you remember what it was for, can be enabled and disabled at any time, and keeps a record of what has been forwarded through it. You can add several destination mailboxes and point different aliases at different ones, block individual senders, and have forwards signed and encrypted with your own PGP key. Bring as many domains as you like, define catch all rules and directory prefixes, and use the built in OpenID Connect provider to sign in to other applications.

The cloudimg image ships the free and open source, AGPL-3.0 licensed SimpleLogin 4.81.6, run unmodified as the official upstream release container under docker compose: the web application, the mail handler and the job runner all on the loopback interface, backed by PostgreSQL 16 which is also loopback only. Host nginx on port 80 is the single public web listener and host Postfix on port 25 is the single public mail listener. Backed by 24/7 cloudimg support.

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

The docker, simplelogin, simplelogin-firstboot, nginx and postfix services all active, the four containers up and healthy, and the deployed application reporting version v4.81.6

What is included:

  • SimpleLogin 4.81.6, the AGPL-3.0 licensed upstream release, run unmodified from the official image pinned by digest
  • The version the deployed application declares is asserted against the pinned version at build time and again after deployment, so a mislabelled upstream tag fails the build rather than shipping
  • nginx on port 80 and Postfix on port 25, both installed as host packages so unattended security upgrades keep the internet facing components patched
  • The web application, the mail handler and PostgreSQL 16 all bound to the loopback interface, so nothing behind the front door is reachable from your network
  • Exactly one administrator, seeded on first boot with a password unique to your VM, and registration closed in the same step
  • The session signing secret, the partner API token secret, the PostgreSQL password, the DKIM key and the OpenID signing key all generated per VM on first boot and written to a root only file
  • A per VM OpenID signing key, because the upstream container image carries one that is committed to a public source repository and the application defaults to it
  • Fail closed guards: both public listeners refuse to start if any secret is empty, unrotated, or a published upstream default
  • simplelogin-set-domain, which repoints the whole appliance at your own domain and prints the exact A, MX, SPF, DKIM and DMARC records to publish
  • The complete corresponding source, its checksum, the AGPL licence text and a written offer at /usr/share/simplelogin/source
  • Ubuntu 24.04 LTS base with latest security patches applied at build time and unattended security upgrades enabled
  • Azure Linux Agent for seamless cloud integration and SSH key injection
  • 24/7 cloudimg support with guaranteed 24 hour response SLA

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 once you add HTTPS) 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 SimpleLogin 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) and SMTP (25). Restrict 22 and 80 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, migrates its database and seeds its administrator automatically.

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 simplelogin-01 \
  --image cloudimg:simplelogin-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 simplelogin-01 --port 80 --priority 1001
az vm open-port --resource-group my-resource-group --name simplelogin-01 --port 25 --priority 1002

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, migrates the database, seeds the administrator and only then opens the public listeners. It normally completes within two minutes of the VM being reachable.

Check the services:

sudo systemctl is-active docker simplelogin simplelogin-firstboot simplelogin-postboot nginx postfix
active
active
active
active
active
active

Check the containers:

sudo docker compose -f /opt/simplelogin/docker-compose.yml ps --format 'table {{.Name}}\t{{.Service}}\t{{.Status}}'
NAME              SERVICE      STATUS
sl-app            app          Up 3 minutes (healthy)
sl-db             db           Up 3 minutes (healthy)
sl-email          email        Up 3 minutes
sl-job-runner     job-runner   Up 3 minutes

Confirm the deployed version and the liveness endpoint:

curl -s http://localhost/version
v4.81.6
curl -s http://localhost/live
live

If the containers are not up yet, watch first boot finish:

sudo journalctl -u simplelogin-firstboot.service -u simplelogin-postboot.service --no-pager -n 20

Step 5: Read your per instance credentials

Every secret on this appliance was generated on your virtual machine at first boot and exists nowhere else. The administrator sign in details are written to a root only file.

sudo cat /root/simplelogin-credentials.txt

The per VM credentials file showing the appliance URL, the seeded administrator address, a masked password, the placeholder alias domain and the DKIM public key, all generated on this virtual machine at first boot

Confirm the file is readable only by root:

sudo stat -c '%a %U:%G' /root/simplelogin-credentials.txt
600 root:root

The same is true of the application configuration, which holds the session signing secret and the database password:

sudo stat -c '%a %U:%G' /opt/simplelogin/simplelogin.env
600 root:root

Step 6: Sign in

Browse to http://<vm-ip>/ and sign in with the address and password from the credentials file.

The SimpleLogin sign in page served by the appliance, asking for an email address and password

Change the password immediately from Settings, then keep the credentials file for reference or delete it.

Step 7: Why nobody else can claim your appliance

This is the single most important difference between this image and a hand rolled install. Self hosted SimpleLogin ships with sign up open and has no notion of an owner, so on a public address any passer by can create an account on your server and start issuing aliases on your domain. Upstream's own instructions tell you to create your account first and close registration afterwards, which leaves a window between the two.

This image never enters that state. First boot seeds exactly one administrator with a password unique to your VM, registration is closed in the configuration from the moment it is written, and neither the web listener nor the mail listener opens until that has happened.

An anonymous sign up attempt is refused:

curl -s -o /dev/null -w 'POST /api/auth/register (anonymous): %{http_code}\n' \
  -X POST -H 'Content-Type: application/json' \
  -d '{"email":"intruder@example.com","password":"Intruder12345"}' \
  http://localhost/api/auth/register
POST /api/auth/register (anonymous): 400

An unauthenticated API request is refused:

curl -s -o /dev/null -w 'GET /api/user_info (anonymous): %{http_code}\n' http://localhost/api/user_info
GET /api/user_info (anonymous): 401

And the refusal is real rather than cosmetic, because the appliance still has exactly one account:

sudo docker exec sl-db psql -U simplelogin -d simplelogin -tAc 'SELECT count(*) FROM users;'
1

An anonymous registration attempt refused with HTTP 400, an unauthenticated API call refused with 401, exactly one account on the appliance, only ports 22, 25 and 80 listening publicly, and the OpenID signing key confirmed as this VM's own rather than the one published in the upstream image

The published private key. The upstream container image carries an OpenID signing key that is committed to SimpleLogin's public source repository, and the application's own defaults point at it. Anyone could therefore forge tokens issued by a stock install. This appliance generates its own key on your VM and refuses to serve if the published one is ever in use:

sudo grep -c '^OPENID_PRIVATE_KEY_PATH=/keys/jwtRS256.key' /opt/simplelogin/simplelogin.env
1

To open registration later, for example so colleagues can have their own accounts, delete the DISABLE_REGISTRATION line from /opt/simplelogin/simplelogin.env and restart the stack. Only do that if the web interface is restricted to networks you trust.

Step 8: Create your first aliases

The dashboard lists every alias, what it was created for, and whether it is currently accepting mail. Random Alias generates one instantly; New Custom Alias lets you choose the local part.

The SimpleLogin alias dashboard listing five aliases on the appliance domain, each with its own description, an enable toggle and a contacts button

The New Custom Alias form, with a prefix field, the alias domain selector, the mailbox that will own the alias, and an optional note

Everything in the interface is also available through the API. Sign in to get an API key, reading the seeded credentials straight from the root only file so no secret is ever typed on a command line:

SL_EMAIL=$(sudo grep -m1 '^SIMPLELOGIN_ADMIN_EMAIL=' /root/simplelogin-credentials.txt | cut -d= -f2-)
SL_PASS=$(sudo grep -m1 '^SIMPLELOGIN_ADMIN_PASSWORD=' /root/simplelogin-credentials.txt | cut -d= -f2-)
SL_KEY=$(curl -s -X POST -H 'Content-Type: application/json' \
  -d "{\"email\":\"${SL_EMAIL}\",\"password\":\"${SL_PASS}\",\"device\":\"guide\"}" \
  http://localhost/api/auth/login | jq -r '.api_key')
test -n "$SL_KEY" && test "$SL_KEY" != null && echo "signed in, API key length ${#SL_KEY}"
signed in, API key length 60

Create an alias for one specific website:

SL_EMAIL=$(sudo grep -m1 '^SIMPLELOGIN_ADMIN_EMAIL=' /root/simplelogin-credentials.txt | cut -d= -f2-)
SL_PASS=$(sudo grep -m1 '^SIMPLELOGIN_ADMIN_PASSWORD=' /root/simplelogin-credentials.txt | cut -d= -f2-)
SL_KEY=$(curl -s -X POST -H 'Content-Type: application/json' \
  -d "{\"email\":\"${SL_EMAIL}\",\"password\":\"${SL_PASS}\",\"device\":\"guide\"}" \
  http://localhost/api/auth/login | jq -r '.api_key')
curl -s -X POST -H "Authentication: ${SL_KEY}" -H 'Content-Type: application/json' \
  -d '{"note":"Created from the cloudimg deployment guide"}' \
  'http://localhost/api/alias/random/new?hostname=example.com' | jq -r '.alias'
example@sl-appliance.example.com

List what you have:

SL_EMAIL=$(sudo grep -m1 '^SIMPLELOGIN_ADMIN_EMAIL=' /root/simplelogin-credentials.txt | cut -d= -f2-)
SL_PASS=$(sudo grep -m1 '^SIMPLELOGIN_ADMIN_PASSWORD=' /root/simplelogin-credentials.txt | cut -d= -f2-)
SL_KEY=$(curl -s -X POST -H 'Content-Type: application/json' \
  -d "{\"email\":\"${SL_EMAIL}\",\"password\":\"${SL_PASS}\",\"device\":\"guide\"}" \
  http://localhost/api/auth/login | jq -r '.api_key')
curl -s -H "Authentication: ${SL_KEY}" 'http://localhost/api/v2/aliases?page_id=0' \
  | jq -r '.aliases[]?.email' | sort
conference-wifi@sl-appliance.example.com
example-shop@sl-appliance.example.com
example@sl-appliance.example.com
forum-example@sl-appliance.example.com
news-weekly@sl-appliance.example.com
simplelogin-newsletter.subpar710@sl-appliance.example.com

Step 9: Point your own domain at the appliance

Until you do this the appliance uses sl-appliance.example.com, a placeholder that can never receive real mail. One command repoints the application, the mail server and the alias domain table together, and prints the DNS records to publish:

sudo simplelogin-set-domain <your-domain>
Repointing this appliance from 'sl-appliance.example.com' to 'your-domain.com'

Publish these DNS records for your-domain.com, then aliases will receive mail:

  A      app.your-domain.com          20.85.234.16
  MX     your-domain.com              10 app.your-domain.com.
  TXT    your-domain.com              "v=spf1 mx ~all"
  TXT    dkim._domainkey.your-domain.com  "v=DKIM1; k=rsa; p=MIIBIjANBgkq..."
  TXT    _dmarc.your-domain.com       "v=DMARC1; p=quarantine; adkim=r; aspf=r"

Set the reverse DNS (PTR) for 20.85.234.16 to app.your-domain.com in your cloud console,
and allow inbound TCP 25 to this VM.

Publish those five records with your DNS provider, then set the VM's reverse DNS in Azure so receiving mail servers accept your forwards:

az network public-ip update \
  --resource-group my-resource-group \
  --name simplelogin-01PublicIP \
  --reverse-fqdn app.<your-domain>

Once DNS has propagated, Postfix accepts mail for your domain and hands it to the alias handler. You can confirm the routing at any time:

ED=$(sudo grep -m1 '^SIMPLELOGIN_EMAIL_DOMAIN=' /root/simplelogin-credentials.txt | cut -d= -f2-)
sudo postmap -q "$ED" pgsql:/etc/postfix/pgsql-relay-domains.cf
sudo postmap -q "$ED" pgsql:/etc/postfix/pgsql-transport-maps.cf
sl-appliance.example.com
smtp:127.0.0.1:20381

Postfix relay domain and transport map lookups resolving the alias domain to the SimpleLogin mail handler on the loopback port, the SMTP banner on port 25, and the AGPL corresponding source archive with a verified checksum

Additional domains are added from the interface, which shows you the records to publish and verifies them for you.

The Custom Domains page, where additional domains are added, verified and used for aliases

Step 10: Mailboxes, the addresses your aliases forward to

A mailbox is a real address that receives the forwarded mail. Your administrator account starts with one, and you can add others and point different aliases at different mailboxes.

The Mailboxes page listing the destination addresses that aliases forward to, with the default mailbox marked

Adding a mailbox sends a verification email to it, so configure outbound mail first (next step).

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

Azure blocks outbound connections on port 25 for most subscriptions. Inbound mail is unaffected, so aliases still receive, but forwards and verification emails will not leave the VM until you relay them through a provider that accepts authenticated submission on port 587.

Check whether outbound 25 works at all on your subscription:

timeout 8 bash -c 'cat < /dev/null > /dev/tcp/aspmx.l.google.com/25' 2>/dev/null \
  && echo "outbound 25 is open" || echo "outbound 25 is blocked, configure a relay below"
outbound 25 is blocked, configure a relay below

To relay, add your provider's submission host and credentials to Postfix:

sudo postconf -e 'relayhost = [smtp.<your-domain>]: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.<your-domain>]:587 <your-relay-user>:<your-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

Make sure the relay is authorised to send for your domain, and include it in your SPF record.

Step 12: Where your data lives

Everything the appliance stores is on the VM.

sudo docker volume ls --filter name=simplelogin --format '{{.Name}}'
simplelogin_sl_db_data
sudo ls -1 /var/lib/simplelogin
bootstrap
keys
sl
upload

Back up the database and the per VM keys together, because the keys are what make your DKIM signatures and OpenID tokens valid:

sudo docker exec sl-db pg_dump -U simplelogin -d simplelogin -Fc -f /tmp/simplelogin.dump \
  && sudo docker cp sl-db:/tmp/simplelogin.dump /var/backups/simplelogin-$(date +%F).dump \
  && sudo ls -l /var/backups/ | tail -2

Copy /var/lib/simplelogin/keys, /opt/simplelogin/simplelogin.env and /root/simplelogin-credentials.txt to secure storage as well. They are the only copies.

Step 13: The complete source is on the machine

SimpleLogin is licensed under the GNU Affero General Public License v3. Section 13 requires that anyone interacting with it over a network can obtain the corresponding source, so it ships on the image.

ls -1 /usr/share/simplelogin/source/
LICENSE
WRITTEN-OFFER.txt
simplelogin-4.81.6-source.tar.gz
simplelogin-4.81.6-source.tar.gz.sha256

The archive is verified against its checksum:

cd /usr/share/simplelogin/source && sha256sum -c simplelogin-4.81.6-source.tar.gz.sha256
simplelogin-4.81.6-source.tar.gz: OK
head -3 /usr/share/simplelogin/source/LICENSE
                    GNU AFFERO GENERAL PUBLIC LICENSE
                       Version 3, 19 November 2007

Step 14: Add HTTPS

The appliance serves plain HTTP on port 80 so it works the moment it boots. Once app.your-domain.com resolves to the VM, add a certificate:

sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app.<your-domain> --agree-tos --redirect -m you@<your-domain>

Then tell SimpleLogin it is being served over HTTPS, so the links it puts in emails are correct:

sudo sed -i 's|^URL=http://.*|URL=https://app.<your-domain>|' /opt/simplelogin/simplelogin.env
sudo docker compose -f /opt/simplelogin/docker-compose.yml restart app

Certbot installs a renewal timer automatically.

Step 15: Day to day operation

Restart the whole stack:

sudo systemctl restart simplelogin.service

Watch the application log:

sudo docker compose -f /opt/simplelogin/docker-compose.yml logs --tail 20 app

Watch the mail handler, which is where forwarding problems show up:

sudo docker compose -f /opt/simplelogin/docker-compose.yml logs --tail 20 email

Check the mail queue:

sudo postqueue -p | tail -3
Mail queue is empty

Confirm exactly which images are running. They are pinned by digest, so this is the precise build of every component, not a moving tag:

sudo docker compose -f /opt/simplelogin/docker-compose.yml images --format json \
  | jq -r '.[] | .ContainerName + "  " + .Repository + "@" + .ID' | sort
sl-app  simplelogin/app-ci@sha256:3abf622d85fbcb090566927c94733ad1a9b1edb215e3a1db61c1e2c116229ee5
sl-db  postgres@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
sl-email  simplelogin/app-ci@sha256:3abf622d85fbcb090566927c94733ad1a9b1edb215e3a1db61c1e2c116229ee5
sl-job-runner  simplelogin/app-ci@sha256:3abf622d85fbcb090566927c94733ad1a9b1edb215e3a1db61c1e2c116229ee5

Security hardening

  • Restrict port 80 and 443 in the network security group to the address ranges that need the interface. Port 25 has to stay open to the internet, but it only speaks SMTP.
  • Add HTTPS (Step 14) before using the appliance over an untrusted network.
  • Change the seeded administrator password at first sign in and enable two factor authentication from Settings.
  • Keep registration closed unless the interface is restricted. An open sign up page on a public address is how a stranger ends up issuing aliases on your domain.
  • Publish SPF, DKIM and DMARC exactly as simplelogin-set-domain prints them. Without them your forwards will be treated as spam.
  • Set reverse DNS on the public IP to match the mail server's hostname.
  • Back up /var/lib/simplelogin/keys with the database. A restored database with a new DKIM key will fail authentication for every message.
  • Enable spam filtering with Spamhaus DQS if you need it. The free public block lists refuse queries from cloud provider resolvers, so they ship disabled rather than silently deferring your mail; the commented block in /etc/postfix/main.cf shows where the DQS key goes.
  • Unattended security upgrades are enabled for the host packages, including nginx and Postfix.

Troubleshooting

The web interface does not answer. The public listener is deliberately closed until first boot has finished. Check sudo systemctl status simplelogin-firstboot.service simplelogin-postboot.service and sudo journalctl -u simplelogin-postboot.service.

nginx or Postfix refuses to start. Both run a fail closed guard first. Run sudo /usr/local/sbin/simplelogin-preflight.sh to see exactly which secret it objected to.

Aliases do not receive mail. Check your MX record points at app.your-domain.com, that inbound 25 is allowed in the network security group, and that sudo postmap -q your-domain.com pgsql:/etc/postfix/pgsql-transport-maps.cf returns smtp:127.0.0.1:20381.

Forwards do not arrive. This is almost always Azure's outbound port 25 block. See Step 11.

Mail is going to spam. Publish SPF, DKIM and DMARC, and set reverse DNS on the public IP.

Support

cloudimg provides 24/7 support with a guaranteed 24 hour response SLA for this image. Contact support@cloudimg.co.uk with your Azure subscription ID and the offer name.

For questions about SimpleLogin itself, see the upstream project.