SignetMail docs Open portal →

#Installing the server

This guide takes you from an empty Linux server to a running, hardened SignetMail on its own HTTPS address. Plan about one hour. The reference environment is a small VPS with Rocky Linux 10; commands for other distributions differ only in the package manager.

Warning. SignetMail inserts text into every e-mail your company sends. Treat the server as a security-critical system: keep it patched, restrict who can reach the portal and protect the administrator accounts with MFA. The steps below include the hardening; do not skip them.

#Overview

  1. Create the server and a cloud firewall.
  2. Point a DNS name at the server.
  3. Harden the operating system (harden.sh).
  4. Configure SignetMail (.env, certificate for Microsoft Graph).
  5. Start the containers.
  6. Register the application in Entra ID (next chapter) and verify.

#1. Create the server

  1. Create a VPS with a recent Rocky Linux (or another Linux with Docker support). 2 vCPU and 4 GB RAM are sufficient.

  2. Add your SSH public key when creating the server. Do not use password login for root.

  3. Create a cloud firewall in your provider's console in front of the server and allow only:

    • TCP 22 from your own IP address only,
    • TCP 80 and 443 from anywhere.

    This is a layer before the server; the server has its own firewall as well.

#2. DNS

Create an A record (and an AAAA record if the server has IPv6) for the public name, for example app.example.com, pointing to the server's IP address. Wait until it resolves:

nslookup app.example.com

Tip. Caddy requests the Let's Encrypt certificate the first time the container starts. If DNS is not yet correct at that moment, the request fails and is retried later; fixing DNS and running sudo docker compose restart caddy is enough.

#3. Harden the operating system

Log in as root once and fetch the deployment files:

dnf -y install git
git clone https://github.com/AljazEferl/signetmail.git /opt/signetmail
cd /opt/signetmail/deploy/cloud
sudo ./harden.sh <admin-user> "<your SSH public key>"

harden.sh is idempotent and does the following:

Area What it does
Admin user Creates <admin-user> with sudo, installs your SSH key.
SSH Key-only login, root login disabled, password authentication disabled — only after you confirm in a second terminal that key login works.
Firewall firewalld with 22 (rate-limited), 80 and 443 only.
Brute force fail2ban for SSH.
Updates Automatic security updates (dnf-automatic).
Kernel sysctl hardening; SELinux stays in enforcing mode.
Docker Installs Docker Engine and the Compose plugin.

The script pauses and asks you to open a second terminal and sign in with the key. Answer da (yes) only when that works; otherwise you could lock yourself out.

After it finishes, work as the admin user (not root) from now on. The files live in /opt/signetmail/cloud (the folder that contains docker-compose.yml).

#4. Configure SignetMail

cd /opt/signetmail/cloud
cp .env.example .env
nano .env

Fill in these values:

Variable Meaning
SIGNETMAIL_VERSION Image tag to run. latest follows the main branch; pin a sha-… tag for controlled updates.
SIGNETMAIL_HOST Public name, for example app.example.com. Must match the DNS record.
ENTRA_TENANT_ID Directory (tenant) ID from the Entra app registration.
ENTRA_CLIENT_ID Application (client) ID of the SignetMail app registration.
PORTAL_OWNER UPN of the first Owner of the portal, for example jane.doe@example.com. Further people are added in the portal under Access.
GRAPH_CERT_PASSWORD Password for the Graph certificate; any long random string.
PORTAL_ALLOWED_IPS Optional. Space-separated IPs/CIDRs allowed to open the portal, for example 203.0.113.10 198.51.100.0/24. Empty = everyone.
BACKUP_GIT_REMOTE Optional. SSH URL of a private repository for encrypted backups.

You will not know ENTRA_TENANT_ID and ENTRA_CLIENT_ID until you complete the Entra registration; it is fine to create the app registration first and come back here.

Generate the certificate SignetMail uses to authenticate to Microsoft Graph:

./setup.sh

This creates data/ and certs/graph.pfx (private key, password protected, readable only by the container user) and certs/graph.cer (public part, valid 730 days). You will upload graph.cer to Entra in the next chapter. Note the expiry date — renewal is described in Operations.

#5. Pull and start

The container image is private. Log in to the GitHub Container Registry once with a personal access token that has only the read:packages scope:

sudo docker login ghcr.io -u <github-user>
sudo docker compose pull
sudo docker compose up -d

Afterwards log out and delete the token if you do not need it for updates:

sudo docker logout ghcr.io

Check that everything runs:

sudo docker compose ps
sudo docker compose logs --tail 50 signetmail-api
curl https://<host>/health

/health returns a small JSON document such as {"status":"ok","lastSyncUtc":"2026-01-01T08:00:00Z"}. A recent lastSyncUtc proves the server has read your directory from Entra ID. It deliberately reveals nothing else.

#What the stack contains

docker-compose.yml defines two services:

  • caddy — reverse proxy on ports 80/443. It sets HSTS, hides the Server header, limits request bodies to 2 MB, blocks /dev paths, optionally restricts the portal to PORTAL_ALLOWED_IPS, and keeps Let's Encrypt certificates in the caddy_data volume.
  • signetmail-api — the application. Not published on the host; only Caddy can reach it. It runs as an unprivileged user (uid 1654) with all Linux capabilities dropped, no-new-privileges, memory and process limits, and rotated logs. Your data/ folder (database, uploaded logos) and certs/ folder (read-only) are mounted into it.

#Next

Register the application in Microsoft Entra ID →

SignetMail documentation · version main