mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
plMail could only sign people in with a local password. On an install that already runs Authentik or Keycloak, that is one more account and password per person, outside the provider's access policy. A second authenticator on the `main` firewall, beside the password form. Both end in the same token for the same User, so the user checker, the login throttle and TOTP two-factor apply unchanged. Authorization code flow with PKCE, state and nonce on league/oauth2-client's GenericProvider; endpoints come from the provider's discovery document. - Set up under a new Admin → SSO tab; the OIDC_* variables are the fallback for an install configured from its compose file, and a saved provider wins whole. This lets an administrator who points it at a provider of their own sign in as any account without a second factor; the security model says so. - The ID token's signature is not verified: it only ever comes from the token endpoint over TLS (OIDC Core § 3.1.3.7 step 6). So the issuer and endpoints must be https and no request to the provider follows a redirect. Accepting an ID token from anywhere else needs JWKS verification first. - An account is linked by (issuer, sub). The address is used for the first link only, and only when the provider reports it verified. - Auto-creation is off by default and creates an ordinary account with no usable password. - app:user:oidc-unlink forgets a link from the console, for somebody deleted and re-added at the provider. Migration: two nullable columns and a unique index on `user`, and a one-row table `oidc_config`. Nothing backfilled; reversible. Not run: Playwright on this branch. No automated test makes a round trip against a real provider; that was checked by hand on a local test stack. Closes #91 Co-Authored-By: Paul Lützner <mail@pluetzner.de> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
275 lines
14 KiB
Bash
275 lines
14 KiB
Bash
# In all environments, the following files are loaded if they exist,
|
|
# the latter taking precedence over the former:
|
|
#
|
|
# * .env contains default values for the environment variables needed by the app
|
|
# * .env.local uncommitted file with local overrides
|
|
# * .env.$APP_ENV committed environment-specific defaults
|
|
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
|
#
|
|
# Real environment variables win over .env files.
|
|
#
|
|
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
|
# https://symfony.com/doc/current/configuration/secrets.html
|
|
#
|
|
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
|
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
|
|
|
###> symfony/framework-bundle ###
|
|
APP_ENV=dev
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# READ THIS BEFORE PUTTING A SECRET IN THIS FILE.
|
|
#
|
|
# docker compose reads .env — this file — to fill in ${VAR} in compose.yaml.
|
|
# That makes anything set here an environment variable inside every container,
|
|
# which in turn tells the entrypoint "the operator supplied this" and switches
|
|
# off the first-run generation for it. A committed value here is therefore a
|
|
# secret shared by every plMail install in existence.
|
|
#
|
|
# So the three that matter are blank, and generated on first start into
|
|
# var/secrets/generated.env. Fill one in only if you mean to manage it
|
|
# yourself — in your shell, or here — and know that it then applies to every
|
|
# service.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
# Signs remember-me cookies, among other things; changing it logs everyone out.
|
|
APP_SECRET=
|
|
APP_SHARE_DIR=var/share
|
|
|
|
# Where the generated secrets live. Must be on a volume EVERY plMail service can
|
|
# see; one that cannot read it mints its own key and stops being able to read the
|
|
# others' data, which `app:secrets:init` detects and refuses to start on. Back
|
|
# this up: losing APP_ENCRYPTION_KEY makes every stored mail credential
|
|
# permanently unreadable.
|
|
APP_SECRETS_FILE=var/secrets/generated.env
|
|
|
|
# Where blobs live, relative to the project root: attachments, raw messages,
|
|
# uploads and avatars all sit under it. One knob so a deployment can put the
|
|
# whole lot on a single mount — see truenas.compose.yaml, which does exactly
|
|
# that. Attachment paths are stored relative to the project root, so changing
|
|
# this on an install with mail in it orphans what is already there.
|
|
APP_STORAGE_DIR=var
|
|
# The address plMail is reached at from outside — what Google and Microsoft
|
|
# call back to with push notifications. Blank on purpose: it is asked for on the
|
|
# setup screen and written to var/secrets/generated.env, because a worker
|
|
# building a push subscription has no request to infer a hostname from. Set it
|
|
# here or in the environment to manage it yourself.
|
|
APP_PUBLIC_URL=
|
|
# The clock a user who has never picked one in Settings is shown. Timestamps
|
|
# are stored in UTC and only ever converted for display, so changing this
|
|
# re-renders existing mail rather than rewriting it. NOT the container's PHP
|
|
# timezone, which is pinned to UTC on purpose — an install left on UTC shows
|
|
# every European user times that are plausibly wrong by their own offset.
|
|
APP_DEFAULT_TIMEZONE=Europe/Berlin
|
|
###< symfony/framework-bundle ###
|
|
|
|
###> symfony/routing ###
|
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
|
DEFAULT_URI=http://localhost
|
|
###< symfony/routing ###
|
|
|
|
###> doctrine/doctrine-bundle ###
|
|
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
|
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
|
#
|
|
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
|
# No password on purpose — see the note at the top of this file. The entrypoint
|
|
# builds the real one from the generated POSTGRES_PASSWORD and exports it, which
|
|
# wins over this.
|
|
#
|
|
# Not blank, though: Doctrine reads the driver out of the scheme, and an empty
|
|
# DSN has none — which fails the prod cache warmup during the Docker build with
|
|
# "could not find driver". A DSN carrying no credentials is not a secret.
|
|
DATABASE_URL="postgresql://app@database:5432/app?serverVersion=18&charset=utf8"
|
|
###< doctrine/doctrine-bundle ###
|
|
|
|
###> symfony/messenger ###
|
|
# Choose one of the transports below
|
|
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
|
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
|
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
|
# The `bulk` queue only, if it should live somewhere other than the line above.
|
|
# Unset it and it follows MESSENGER_TRANSPORT_DSN, which is what every real
|
|
# deployment wants; it exists so the browser suite can make that one queue a
|
|
# real, consumed one while the rest stay in memory (see compose.test.yaml).
|
|
# MESSENGER_BULK_DSN=doctrine://default?auto_setup=0
|
|
###< symfony/messenger ###
|
|
|
|
###> symfony/mailer ###
|
|
MAILER_DSN=null://null
|
|
###< symfony/mailer ###
|
|
|
|
###> app/encryption ###
|
|
# Base64-encoded 32-byte libsodium secretbox key used by the `encrypted_string`
|
|
# Doctrine type to encrypt account credentials at rest. SEPARATE from APP_SECRET.
|
|
#
|
|
# Blank on purpose — see the note at the top of this file. Generated on first
|
|
# start, and worth backing up: it is the only thing that can read the mailbox
|
|
# passwords and OAuth tokens in your database.
|
|
APP_ENCRYPTION_KEY=
|
|
###< app/encryption ###
|
|
APP_DEV_USER_EMAIL=
|
|
APP_DEV_USER_PASSWORD=
|
|
|
|
###> app/demo ###
|
|
# Turns the whole instance into a public demo: every visitor is handed a
|
|
# throwaway user with a seeded mailbox, sending is swallowed and answered by a
|
|
# script, and a button delivers scripted mail on demand.
|
|
#
|
|
# Instance-wide rather than per-user on purpose. Demo mode's promise is that
|
|
# nothing here reaches the network, and that is only checkable if there is one
|
|
# switch — a per-account flag would mean auditing every outbound path for
|
|
# whether it honoured it. With this on, mail sync, push renewal, calendar sync
|
|
# and real account creation are all off for everybody.
|
|
#
|
|
# NEVER set this on an instance holding real mail. See docs/demo-mode.md.
|
|
APP_DEMO_MODE=0
|
|
# How long a visitor's throwaway user lives before app:demo:reap removes it.
|
|
APP_DEMO_TTL=PT2H
|
|
|
|
# Impressum / legal notice, linked from the login page in demo mode.
|
|
#
|
|
# A public demo hosted in Germany needs one (§ 5 TMG), and it has to name a real
|
|
# person or company — so these are blank here and the page says loudly that they
|
|
# are unset rather than inventing a plausible-looking operator. Nobody but the
|
|
# operator can fill these in.
|
|
#
|
|
# Ignored entirely when APP_DEMO_MODE is off: the route does not exist there.
|
|
APP_DEMO_IMPRESSUM_NAME=
|
|
APP_DEMO_IMPRESSUM_ADDRESS=
|
|
APP_DEMO_IMPRESSUM_EMAIL=
|
|
# Named in the privacy notice as the processor running the server. Blank shows
|
|
# a warning on the page rather than a guess — see APP_DEMO_IMPRESSUM_NAME.
|
|
APP_DEMO_PRIVACY_HOST=
|
|
###< app/demo ###
|
|
|
|
###> symfony/mercure-bundle ###
|
|
# See https://symfony.com/doc/current/mercure.html#configuration
|
|
# The URL of the Mercure hub, used by the app to publish updates (can be a local URL)
|
|
MERCURE_URL=http://mercure/.well-known/mercure
|
|
MERCURE_PUBLIC_URL=https://localhost/.well-known/mercure
|
|
# Blank on purpose — see the note at the top of this file. Generated on first
|
|
# start, and read by both the app and the hub container from the same file.
|
|
MERCURE_JWT_SECRET=
|
|
###< symfony/mercure-bundle ###
|
|
|
|
###> google oauth ###
|
|
GOOGLE_OAUTH_CLIENT_ID=
|
|
GOOGLE_OAUTH_CLIENT_SECRET=
|
|
GMAIL_PUBSUB_TOPIC=projects/your-project-id/topics/gmail-push
|
|
GMAIL_PUBSUB_VERIFICATION_TOKEN=
|
|
###< google oauth ###
|
|
|
|
###> microsoft/graph ###
|
|
MICROSOFT_OAUTH_CLIENT_ID=
|
|
MICROSOFT_OAUTH_CLIENT_SECRET=
|
|
# common | organizations | consumers | <tenant-guid>
|
|
# Must match the app registration's supported-account-types, or consent fails
|
|
# with AADSTS50194. `organizations` avoids the consumer-account edge cases
|
|
# (no immutable ids, reduced $filter support) if you don't need outlook.com.
|
|
MICROSOFT_OAUTH_TENANT=common
|
|
###< microsoft/graph ###
|
|
|
|
###> plmail/oidc ###
|
|
# Single sign-on through an OpenID Connect provider — Authentik, Keycloak,
|
|
# Authelia and the like. Set up under Admin → SSO; these are the fallback, read
|
|
# only while no complete provider is saved there. Off while any of the first
|
|
# three is blank and nothing is saved; otherwise the login page offers a "Sign
|
|
# in with …" button beside the password form. Register this redirect URI at the
|
|
# provider:
|
|
# https://<your plMail address>/login/oidc/callback
|
|
#
|
|
# The issuer is the provider's, exactly as its discovery document states it —
|
|
# in Authentik, the "OpenID Configuration Issuer" on the provider's page, e.g.
|
|
# https://auth.example.com/application/o/plmail/ . It must be https.
|
|
OIDC_ISSUER=
|
|
OIDC_CLIENT_ID=
|
|
OIDC_CLIENT_SECRET=
|
|
# What the button calls the provider ("Sign in with Authentik"). Blank says
|
|
# "single sign-on".
|
|
OIDC_BUTTON_LABEL=
|
|
# Space-separated; `openid` is always added. `email` is what lets a first
|
|
# sign-in find the account it belongs to.
|
|
OIDC_SCOPES="openid email profile"
|
|
# Create an account for somebody the provider lets in and plMail has never
|
|
# seen. Off, an administrator creates the account first, with the same address.
|
|
OIDC_AUTO_CREATE=false
|
|
# Link an existing account by address only when the provider says it verified
|
|
# that address (`email_verified`). Turning this off trusts the provider's
|
|
# address blindly: whoever can edit their own address there can claim any
|
|
# account here that has not signed in through the provider yet. Only `false`,
|
|
# `0`, `no` or `off` turn it off; blank or anything else leaves it on.
|
|
OIDC_REQUIRE_VERIFIED_EMAIL=true
|
|
###< plmail/oidc ###
|
|
|
|
APP_DB_LOG_LEVEL=warning
|
|
APP_CONTAINER_NAME=web
|
|
|
|
# WIDE ON PURPOSE, AND WORTH NARROWING. Loopback plus every private range,
|
|
# because a reverse proxy on the same Docker network or LAN has an address in
|
|
# one of them that cannot be known here — and a proxy that is NOT in this list
|
|
# breaks sign-in with OAuth, shares one login allowance between everybody and
|
|
# strips `secure` off cookies, none of which says why.
|
|
#
|
|
# The cost of the width: anything else with a private address that can reach
|
|
# plMail directly — another machine on the LAN, another container — is believed
|
|
# when it sends X-Forwarded-For, and can pose as any address it likes. That
|
|
# gets it past the PER-ADDRESS limits on the login form and the public booking
|
|
# page. It does not get it past the per-account login limit, which ignores the
|
|
# address. Once your proxy works, replace this with the proxy's own address or
|
|
# subnet; docs/install/reverse-proxy.md says how to find it.
|
|
TRUSTED_PROXIES=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
|
|
|
###> lexik/jwt-authentication-bundle ###
|
|
# Beside the generated secrets rather than in config/jwt: the keys are not in
|
|
# the image (they are gitignored), so each service would otherwise generate its
|
|
# own and reject tokens the others signed. Generated by `app:secrets:init`.
|
|
#
|
|
# Deliberately empty: config/bootstrap_generated_secrets.php puts both in a
|
|
# jwt/ directory next to APP_SECRETS_FILE, so they follow the secrets onto
|
|
# whichever volume a deployment keeps those on. Set either to override.
|
|
JWT_SECRET_KEY=
|
|
JWT_PUBLIC_KEY=
|
|
# Real passphrase lives in .env.local (untracked); config/jwt/*.pem is gitignored.
|
|
JWT_PASSPHRASE=
|
|
###< lexik/jwt-authentication-bundle ###
|
|
|
|
###> plmail/web-push ###
|
|
# VAPID keypair for JMAP PushSubscription + browser Web Push (RFC 8292).
|
|
# Generate with: bin/console app:push:generate-vapid-keys
|
|
# The PRIVATE key belongs in .env.local (untracked) — never here.
|
|
# In a container deployment there is no .env.local: set these as environment
|
|
# variables on the service instead (see truenas.compose.yaml x-config). A real
|
|
# env var overrides whatever "composer dump-env prod" baked into the image.
|
|
VAPID_SUBJECT=mailto:admin@example.com
|
|
VAPID_PUBLIC_KEY=
|
|
VAPID_PRIVATE_KEY=
|
|
# Comma-separated hosts a push endpoint may point at even though they resolve
|
|
# to a private address. Every other private, loopback or link-local endpoint is
|
|
# refused, because the server POSTs to it on every change. Set it to your
|
|
# SERVER_NAME if you run the bundled ntfy (compose --profile push) on a LAN or
|
|
# Tailscale address, e.g. "192.168.1.10" or "mail.lan".
|
|
PUSH_ALLOWED_HOSTS=
|
|
###< plmail/web-push ###
|
|
|
|
###> plmail/integrations ###
|
|
# Integration provider credentials live in the database, not here — admins edit
|
|
# them under /admin?section=integrations. These two are the SSRF guard rails
|
|
# around the server address a user may type in for a self-hosted service.
|
|
#
|
|
# Self-hosted Nextcloud and Immich on a LAN often have no certificate, so
|
|
# allowing plain http is common. It is off by default so that sending an app
|
|
# password in the clear stays a deliberate choice rather than an accident.
|
|
INTEGRATIONS_ALLOW_HTTP=false
|
|
# Comma-separated hosts exempt from the private-range block, for reaching a
|
|
# service on the same Docker network or LAN, e.g. "nextcloud.lan,10.0.0.5".
|
|
# Everything else resolving into loopback, link-local or RFC1918 is refused —
|
|
# that block is what stops a user aiming plMail at the database container or
|
|
# at 169.254.169.254. It is checked on every request and every redirect, not
|
|
# only when the address is saved. A server address an admin pins under
|
|
# Admin → Integrations is exempt the same way, without being listed here.
|
|
INTEGRATIONS_ALLOWED_HOSTS=
|
|
###< plmail/integrations ###
|