Copyright (C) 2026 Pim Messelink <g-2eebed68-guise@club77.org> Licensed under the GNU Affero General Public License v3.0 or later. See
LICENSE.
Self-hosted web app for managing per-recipient email aliases on a docker-mailserver instance, without SSH. Generates random alias addresses labeled with the service they're for, e.g. g-a3f82c11-netflix@example.com, and routes them to your real mailbox. Bitwarden's Forwarded email alias generator can create them directly from any sign-up form (via a SimpleLogin-compatible API; see below).
Auth piggybacks on the mailserver itself (IMAP), so there's no separate user database. Aliases live in postfix-virtual.cf, and the embedded label travels in the address itself — guise owns no application data of its own beyond a regenerable session key.
A sub-addressed address (e.g. user+netflix@example.com) reveals your real mailbox to anyone who reads it. If one service leaks its user list and addresses follow user+<service>@…, an attacker can probe user+<every-other-service>@… to link your identity across services. Random-prefix aliases break both: g-a3f82c11-netflix@example.com reveals nothing about the mailbox and doesn't correlate with g-5b2e1c8a-bank@example.com. The embedded label is just operator ergonomics — sub-addressing has "which alias did I use here?" for free, and the label is how guise gets that back.
guise/
├── README.md this file
├── LICENSE AGPL-3.0
├── CHANGELOG.md
├── CONTRIBUTING.md
├── SECURITY.md
├── docs/ api.md — SimpleLogin-compatible HTTP API spec
├── screenshots/ login + dashboard
├── .github/workflows/ GitHub Actions CI (pytest, CodeQL, release)
└── server/ Python/Flask source for the guise Docker image
server/ builds the image; the running deployment is a sidecar service inside your docker-mailserver compose project. Prebuilt images are published to GitHub Container Registry at ghcr.io/messelink/guise for linux/amd64 and linux/arm64.
Add the two services below to your docker-mailserver compose.yaml, alongside the existing mailserver service. The guise-socket-proxy sidecar restricts guise's Docker API access to only the container-listing and exec endpoints it needs to write aliases — an RCE in guise can no longer touch the host Docker daemon directly. See server/README.md for the trust-boundary rationale.
guise-socket-proxy:
image: tecnativa/docker-socket-proxy:latest
container_name: guise-socket-proxy
restart: always
environment:
CONTAINERS: 1 # allow GET on /containers/*
EXEC: 1 # allow POST on /containers/{id}/exec and /exec/{id}/start
POST: 1 # allow POST requests in general
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# not exposed on the host; only reachable from the project's docker network
guise:
image: ghcr.io/messelink/guise:latest
container_name: guise
restart: always
ports: ["127.0.0.1:9100:8000"]
volumes:
- ./guise-data:/data
environment:
GUISE_DOMAIN: example.com # your mail domain
GUISE_TAG: g-
GUISE_DENIED_USERS: noreply,admin,bot # service accounts to block
GUISE_MAILSERVER_CONTAINER: mailserver
GUISE_IMAP_HOST: mailserver
GUISE_IMAP_PORT: "993"
DATA_DIR: /data
SESSION_COOKIE_SECURE: "true"
DOCKER_HOST: tcp://guise-socket-proxy:2375
depends_on:
- mailserver
- guise-socket-proxyThen from your docker-mailserver compose project directory:
mkdir -p guise-data
docker compose pull guise guise-socket-proxy
docker compose up -d guise guise-socket-proxy
To pin a specific version instead of tracking :latest, use ghcr.io/messelink/guise:0.3.0 (or any tag from the Releases page). Upgrades are then a docker compose pull guise && docker compose up -d --force-recreate guise.
If you'd rather not pull a prebuilt image:
cd guise/server
make build # produces local guise:latest
…then use image: guise:latest in the compose block above.
guise listens on container port 8000, mapped to 127.0.0.1:9100 on the host by default. Anything that can terminate TLS and forward to it works.
If you already have a reverse proxy on the host (system Apache, Caddy, nginx, …), point it at 127.0.0.1:9100. The simplest Caddyfile entry:
guise.example.com {
reverse_proxy 127.0.0.1:9100
}If you have an existing reverse-proxy container on the same compose project, point it at guise:8000 directly (docker-network DNS), bypassing the host-port mapping.
If you don't already have any reverse proxy, drop a compose.override.yaml alongside your compose.yaml:
services:
reverse-proxy:
image: caddy:2-alpine
container_name: guise-reverse-proxy
restart: always
ports: ["80:80", "443:443"]
volumes:
- caddy-data:/data
configs:
- source: caddy-config
target: /etc/caddy/Caddyfile
depends_on:
- guise
volumes:
caddy-data:
configs:
caddy-config:
content: |
guise.example.com {
reverse_proxy guise:8000
}docker compose up -d auto-merges *.override.yaml. Caddy auto-provisions a Let's Encrypt cert for guise.example.com, redirects HTTP → HTTPS, and forwards to guise over the docker network. To disable, delete the file.
Header trust: guise enables ProxyFix (x_for=1, x_proto=1, x_host=1) so request.remote_addr reflects the real client IP from X-Forwarded-For and request.is_secure reflects HTTPS termination at the proxy. This is correct only if guise is reached exclusively through the reverse proxy. If guise is also reachable directly (e.g. you bind it to 0.0.0.0:9100), a malicious client can spoof those headers — keep the bind on 127.0.0.1 and front everything through the proxy.
guise exposes the subset of the SimpleLogin REST API that password-manager "forwarded email alias" generators rely on. Any client that supports pointing at a self-hosted SimpleLogin server should be able to create guise aliases without modification.
Point your client at https://guise.example.com; the API key is your mailbox short-username and IMAP password joined by : — same auth path as the web UI, no separate token to manage. For example, if your mailbox is alice@example.com and your IMAP password is s3cret-imap-password, the API key is alice:s3cret-imap-password.
Bitwarden is the verified-working client (browser extension, mobile, desktop). Any other SimpleLogin client with a self-hosted server URL setting should also work — open an issue if you confirm one. Auto-labeling from the request URL is opt-out per instance via GUISE_API_AUTOLABEL=0.
Full request/response spec, error codes, and the SimpleLogin subset implemented are in docs/api.md.
- Browse to
https://guise.example.com/login - Log in with your short mailserver username + password
- Dashboard shows two sections:
- Managed by guise — addresses starting with
g-, with their labels - Other aliases routing to you — anything else in
postfix-virtual.cfpointing to your address (pre-existing aliases). Deleting one of these prompts an extra confirmation.
- Managed by guise — addresses starting with
- Type a label, click Create alias → fresh
g-<8 hex>-<label>address appears, ready to copy. - Click delete on any alias to remove it.
- In Bitwarden, configure Username Generator → Forwarded email alias → SimpleLogin (self-hosted server). Server URL:
https://guise.example.com. API key:<your-username>:<your-imap-password>(your mailbox short-username and IMAP password joined by a colon). - On any sign-up form, focus the email/username field. Bitwarden's in-page bubble offers Generate: pick Forwarded email alias → a fresh
g-<random>-<site>@<domain>alias appears and gets pasted. - Manage or delete the alias later via the web UI's Managed by guise section.
docker compose stop guise guise-socket-proxy
docker compose rm -f guise guise-socket-proxy
# remove both service blocks from compose.yaml
rm -rf guise-data
docker image rm ghcr.io/messelink/guise:latest tecnativa/docker-socket-proxy:latest
Mailserver is untouched.
See server/README.md for build/test details.


