mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 10:04:18 +00:00
#34 points out that the default trusts every private range, so a machine on the LAN that reaches plMail directly can forge X-Forwarded-For and step around the per-address limits on the login form and the booking page. The default stays. The proxy's own address cannot be known here, and a proxy missing from the list breaks OAuth sign-in, shares one login allowance between everybody and strips `secure` off cookies, with nothing saying why — a worse failure, on every proxied install, than the one being closed. What was missing is the sentence: the comment in .env and the reverse-proxy page now say that the default is wide, what that costs, what it does not (the per-account login limit ignores the address and still holds), and how to narrow it once the proxy works. SECURITY.md is new. The report was filed publicly because there was nowhere private to send it. Refs #34 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
243 lines
12 KiB
Bash
243 lines
12 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 ###
|
|
|
|
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 ###
|