Deployment guide

SimpleLogin

Overview

This guide covers the deployment and configuration of SimpleLogin on Ubuntu 24.04 on Azure, using SecureAnyCloud Azure Marketplace images. SimpleLogin 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. You can reply from the alias, so the other side never learns where your mail 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 receives spam tells you exactly who leaked or sold it.

You can create aliases from the web interface, from the official browser extensions and mobile apps, or through the API. Each alias carries a note so that you remember what it was for. You can enable and disable it at any time, and it keeps a record of what has been forwarded through it.

You can add several destination mailboxes and point different aliases at different ones. You can block individual senders and have forwards signed and encrypted with your own PGP key. You can 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 SecureAnyCloud image ships SimpleLogin 4.81.6, which is free and open source under the AGPL-3.0 licence. It runs unmodified as the official upstream release container under docker compose. The web application, the mail handler and the job runner are 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. The image is backed by 24/7 SecureAnyCloud support.

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

What you get:

  • SimpleLogin 4.81.6, the AGPL-3.0 licensed upstream release, run unmodified from the official image pinned by digest
  • A version check: the version that 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 that is 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 SecureAnyCloud support with a 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
1

Deploy from the Azure Portal

  • Sign in to the Azure Portal and choose Create a resource.
  • Search the Marketplace for SimpleLogin 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) and SMTP (25). Restrict 22 and 80 to your own address ranges. Port 25 must be open to the internet for mail delivery to work.
  • Review and create the VM. It boots, generates its own secrets, migrates its database and seeds its administrator automatically.
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:simplelogin-ubuntu-24-04:default:latest
az vm create \
  --resource-group my-resource-group \
  --name simplelogin-01 \
  --image sac1647283583153: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 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

First boot generates every secret, migrates the database and seeds the administrator. Only then does it open 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
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. Read them with:

sudo cat /root/simplelogin-credentials.txt

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
6

Sign in

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

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

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. 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 that is unique to your VM. Registration is closed in the configuration from the moment it is written. 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

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

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. Do this only if the web interface is restricted to networks that you trust.

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.

Everything in the interface is also available through the API. Sign in to get an API key. Read the seeded credentials straight from the root-only file, so that 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 sac 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
9

Point your own domain at the appliance

Until you do this, the appliance uses sl-appliance.example.com, a domain 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 that 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 with:

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

To add more domains, use the interface. It shows you the records to publish and verifies them for you.

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. You can add others and point different aliases at different mailboxes.

Adding a mailbox sends a verification email to it, so configure outbound mail first (see the next 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 mail. 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 that the relay is authorised to send for your domain, and include it in your SPF record.

12

Where your data lives

Everything the appliance stores is on the VM. These commands show where:

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, because they are the only copies.

13

The complete source is on the machine

SimpleLogin is licensed under the GNU Affero General Public License v3. Section 13 requires that anyone who interacts with it over a network can obtain the corresponding source, so the source 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

Verify the archive 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
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 that it is served over HTTPS, so that 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.

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 log, 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 the output shows 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 ports 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 you use 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 the first boot services with 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 that 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

SecureAnyCloud provides 24/7 support with a guaranteed 24-hour response SLA for this image. Contact sales@secureanycloud.com with your Azure subscription ID and the offer name.

For questions about SimpleLogin itself, see the upstream project.

Deploy on Azure

Launch SimpleLogin 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