Skip to main content

Security and hardening

This page collects the security decisions for a self-hosted Kanera deployment. It covers what faces the internet, which secrets matter, what the server contacts, and which settings to tighten once the stack is running.

Read it after Getting Started. The detailed settings live on the other self-host pages, which are linked from each section.

What faces the internet

The bundled docker-compose.yml publishes none of the application services. You decide what to publish, usually through a compose.override.yml and a reverse proxy. See Services and Routing.

ServiceExposureNotes
webPublic, behind your HTTPS proxyThe only service browsers need. Its nginx forwards app API and realtime traffic to api internally.
public-apiOptional, publicPublish it only for integrations, the API reference, or AI agent OAuth.
mcpOptional, publicPublish it only if remote AI clients should connect.
apiInternal onlyNever give the main API its own public domain.
workerInternal onlyRuns schedulers and outbound delivery.
postgresHost loopback onlyBound to 127.0.0.1:5433 by default (POSTGRES_BIND_PORT). Use an SSH tunnel for remote access.
valkeyInternal onlyNo published port. It runs without a password on the Compose network, so never publish it.
admin-apiOff by defaultStarts only with COMPOSE_PROFILES=admin. Put it on its own domain if you enable it.
MonitoringOff by defaultWith the monitoring profile, Prometheus and Grafana bind to 127.0.0.1 unless you set MONITORING_BIND_IP, for example to a VPN address. Never bind them to 0.0.0.0.

The fewer services you publish, the less there is to attack. A deployment that uses only the web app needs a single public domain.

TLS and the reverse proxy

Kanera's containers speak plain HTTP. Terminate TLS at your reverse proxy or load balancer (Caddy, Traefik, nginx, Dokploy, or a cloud load balancer), and serve every public domain over HTTPS.

  • Set COOKIE_SECURE=true, and make WEB_ORIGIN and COOKIE_DOMAIN match the public HTTPS address.
  • The web container sends Strict-Transport-Security with includeSubDomains and a one-year lifetime. Browsers will then insist on HTTPS for subdomains of the app domain too, so keep API, MCP, and admin subdomains on HTTPS.
  • It also sends a Content Security Policy, X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, and a restrictive Permissions-Policy. Don't strip them at the proxy.

Rate limits and sign-in throttles use the client IP. That IP is only trustworthy when a proxy overwrites the forwarding headers:

SettingCompose defaultSet to true when
API_TRUST_PROXYfalseThe app sits behind a proxy that sets the real client IP.
PUBLIC_API_TRUST_PROXYfalseThe same, for the public API.
MCP_TRUST_PROXYtrueLeave it true only while MCP is reachable solely through such a proxy.
ADMIN_TRUST_PROXYtrueLeave it true only while the admin API is reachable solely through such a proxy.

If a service trusts proxy headers but clients can reach it directly, they can forge their IP and get around per-IP limits.

Secrets

Generate each secret separately, for example with openssl rand -hex 32, and keep them in a secret store outside the server as well as in .env. In production, the API refuses to start while known development placeholders are still in use. It also refuses to start when SECRETS_ENCRYPTION_KEY is missing or equal to JWT_SECRET, or when MCP_INTERNAL_SECRET or ADMIN_JWT_SECRET equals JWT_SECRET.

SecretProtectsIf you change it
POSTGRES_PASSWORDThe database loginUpdate the running database user too.
JWT_SECRETApp sessionsEveryone is signed out. This is the quickest way to end every session after a suspected leak.
MFA_ENCRYPTION_KEYAuthenticator secrets and recovery codesEvery enrolled authenticator stops working.
MEDIA_SIGNING_SECRETSigned media URLsExisting media links stop resolving.
SECRETS_ENCRYPTION_KEYStored SMTP, storage, webhook, chat destination, and GitHub App credentialsStored credentials can no longer be decrypted and must be entered again.
MCP_INTERNAL_SECRETThe private exchange between mcp and public-apiSet the same new value on both services. In-flight MCP requests may fail.
ADMIN_JWT_SECRETManagement portal sessionsPortal admins are signed out.
METRICS_TOKEN/metrics scrapingUpdate Prometheus to match.
DB_BACKUP_ENCRYPTION_PASSPHRASEEncrypted backupsBackups made with the old passphrase still need the old passphrase.

Kanera has no built-in way to re-encrypt stored data under a new key. Treat MFA_ENCRYPTION_KEY and SECRETS_ENCRYPTION_KEY as long-lived, and plan a rotation before you need one. See Secrets worth keeping.

Metrics are on by default, but /metrics returns 404 until METRICS_TOKEN is set. The monitoring profile refuses to start without METRICS_TOKEN and GRAFANA_ADMIN_PASSWORD.

Outbound traffic

Kanera has no update check. Product analytics is off by default and intended only for Kanera Cloud. The server contacts only destinations that you, your admins, or your users configure:

  • Your SMTP server, S3-compatible storage, and backup bucket.
  • Browser push services, once an organisation admin enables push.
  • Workspace webhooks and chat destinations (Slack, Discord, Telegram, Zulip) set up by workspace admins.
  • Personal ntfy, Gotify, or webhook channels set up by users.
  • ALERT_WEBHOOK_URL for operational alerts, if set.
  • Trello, during an import that copies attachments with TRELLO_API_KEY, and GitHub, when a GitHub App is configured for link previews.
  • Cloudflare Turnstile, only when its keys are set. Leave them unset for normal self-hosting.

One destination is not operator-configured. When an AI client calls the MCP search.docs tool, the MCP service downloads the public documentation index from https://www.kanera.app/docs-search.json and searches it locally. The search query is not sent.

Private networks

Workspace webhooks and chat destinations must use public HTTPS addresses. Kanera refuses loopback, private, CGNAT, link-local and cloud metadata, and other reserved addresses. It checks the address again at delivery time and does not follow redirects. That stops a webhook or chat destination from being pointed at services inside your network.

Personal notification channels follow the same rule unless you opt in:

KANERA_ALLOW_PRIVATE_NOTIFICATION_DESTINATIONS=true

This lets users send ntfy, Gotify, and personal webhook notifications to http:// and private-network receivers. Link-local and metadata addresses, multicast, and other reserved ranges stay blocked. It does not relax the rule for workspace webhooks or chat destinations. Turn it on only if you accept that any user can make the server send requests to hosts on your internal network. See Email and Notifications.

Backups

The optional db-backup service encrypts each PostgreSQL dump with GPG symmetric AES-256 before uploading it to S3-compatible storage. A manual pg_dump is not encrypted, so encrypt it yourself before it leaves the server. Uploaded files in the kanera_uploads volume are not covered by db-backup and need their own backup. Keep backups and the backup passphrase off the server's primary disk. See Storage and Backups.

Updates

Security fixes are provided for the current main branch and the latest tagged release. Stay on one of those, read the release notes, and back up before updating. Third-party images in the compose file are pinned to exact versions, so a routine redeploy doesn't change PostgreSQL or Valkey without a reviewed change. See Updates and Scaling.

Accounts and sign-in

  • Close public signup. After creating your organisation, set SIGNUPS_ENABLED=false. Invitation links keep working.
  • Verify email addresses. Once outbound mail works, set EMAIL_VERIFICATION_ENABLED=true.
  • Keep auth throttling on. AUTH_RATE_LIMIT_ENABLED defaults to true, allowing 10 attempts per minute per IP.
  • Require MFA. Organisation admins can require an authenticator for everyone under Profile settings -> Organisation. See Profile and Security.
  • Session lifetimes. App access tokens last 5 minutes and refresh sessions 10 days (JWT_ACCESS_TTL, JWT_REFRESH_TTL_DAYS).
  • Management portal. It is off by default and uses its own accounts and secret. If you enable it, its support sessions act as a target organisation's owner. Each one is recorded in an audit log and expires on its own, after 60 minutes by default (SUPPORT_SESSION_TTL_MINUTES, at most 480).

AI agent access

Expose mcp and the OAuth routes on public-api only if you want remote AI clients. Plan gating doesn't apply to self-hosted deployments, so once MCP is reachable, any member can connect an agent that acts with that member's permissions.

  • Set MCP_SERVER_PUBLIC_URL and PUBLIC_API_OAUTH_ISSUER to your own domains. OAuth tokens are bound to that MCP address and rejected by /api/v1.
  • Any client can register itself at /oauth/register. Registering grants nothing until a signed-in user approves the connection on the consent screen.
  • Authenticated public API and MCP requests are limited to 400 per minute per credential by default. Failed key attempts are limited to 10 per minute per IP. See Environment Reference.
  • The MCP service logs each tool call's name, duration, and outcome, but not its arguments.

See Integrations and APIs for setup and Agent permissions and security for what an agent can do.

Report a vulnerability

Do not open a public issue for a security problem. Email [email protected] with a description, reproduction steps, the affected component, and whether it affects the default self-hosted configuration. Reports are acknowledged within five business days. See SECURITY.md in the Kanera repository for scope and disclosure.