Secret management overview
All secrets live in the.env file at the repository root. Services consume them through Docker Compose env_file directives and config templates processed by scripts/prepare-config.sh via envsubst. The .env file is listed in .gitignore and must never be committed.
File permissions
Lock down your secrets files immediately after creation:Generating secure values
Every sensitive environment variable in.env.example ships with a placeholder value (e.g. change_me_*). You must replace each one before deploying to production.
IRC cloak keys (IRC_CLOAK_KEY_1, IRC_CLOAK_KEY_2, IRC_CLOAK_KEY_3)
Cloak keys mask user IP addresses. All three keys must be identical across every IRC server in your network. Use the built-in just recipe:
scripts/gencloak-update-env.sh, which invokes UnrealIRCd’s gencloak command inside the container, parses the three keys, updates .env, and re-runs prepare-config.sh.
IRC operator password (IRC_OPER_PASSWORD)
UnrealIRCd requires an Argon2-hashed password for IRC operators. Generate one interactively:
.env:
IRC die/restart password (IRC_DRPASS)
This password protects the /DIE and /RESTART IRC commands. Generate a random value:
.env:
IRC services password (IRC_SERVICES_PASSWORD)
Shared secret between UnrealIRCd and Atheme for the services link. Generate a strong random value:
WebIRC passwords (ATL_WEBIRC_PASSWORD, THELOUNGE_WEBIRC_PASSWORD)
WebIRC passwords authenticate web clients (The Lounge, the Next.js web app) to UnrealIRCd so that real user IPs are forwarded. Generate a unique value for each:
WebPanel RPC password (WEBPANEL_RPC_PASSWORD)
Authenticates the WebPanel to UnrealIRCd’s JSON-RPC interface:
Bridge secrets
| Variable | Purpose | Generation |
|---|---|---|
BRIDGE_DISCORD_TOKEN | Discord bot token | Create at Discord Developer Portal → Bot → Reset Token |
BRIDGE_PORTAL_TOKEN | Portal API authentication | openssl rand -base64 32 |
BRIDGE_XMPP_COMPONENT_SECRET | XMPP component authentication | openssl rand -base64 32 |
BRIDGE_IRC_OPER_PASSWORD | Bridge IRC oper credentials | openssl rand -base64 32 |
Prosody (XMPP) secrets
| Variable | Purpose | Generation |
|---|---|---|
PROSODY_DB_PASSWORD | PostgreSQL database password (production only) | openssl rand -base64 32 |
PROSODY_OAUTH2_REGISTRATION_KEY | OAuth2 client registration key | openssl rand -base64 32 |
TURN secret (TURN_SECRET)
Shared secret for TURN/STUN authentication:
Cloudflare DNS API token (CLOUDFLARE_DNS_API_TOKEN)
Required only for production TLS via Let’s Encrypt DNS-01 challenges. Create a scoped API token in the Cloudflare dashboard with Zone:DNS:Edit permission for your domain. Not needed for local development.
Alchemy password (ALCHEMY_PASSWORD)
Used by the docs deployment (Cloudflare Workers via Alchemy) to encrypt deployment state secrets:
Portal integration passwords
These variables are consumed by the external ATL Portal service, not by this monorepo’s compose stack:| Variable | Purpose | Generation |
|---|---|---|
IRC_UNREAL_RPC_PASSWORD | Portal → UnrealIRCd JSON-RPC | Should match WEBPANEL_RPC_PASSWORD |
PROSODY_REST_PASSWORD | Portal → Prosody REST API | openssl rand -base64 32 |
Credential rotation
Rotate all secrets on a regular schedule (every 6–12 months) or immediately after a suspected compromise.Rotation procedure
- Generate a new value using the methods above
-
Update
.envwith the new value -
Re-generate config templates:
-
Restart affected services:
-
Verify the service is healthy:
Coordinated rotation
Some secrets are shared between services and must be updated in lockstep:| Secret | Services that share it |
|---|---|
IRC_SERVICES_PASSWORD | UnrealIRCd ↔ Atheme |
ATL_WEBIRC_PASSWORD | UnrealIRCd ↔ Next.js web app |
THELOUNGE_WEBIRC_PASSWORD | UnrealIRCd ↔ The Lounge |
WEBPANEL_RPC_PASSWORD | UnrealIRCd ↔ WebPanel |
BRIDGE_XMPP_COMPONENT_SECRET | Prosody ↔ Bridge |
BRIDGE_IRC_OPER_PASSWORD | UnrealIRCd ↔ Bridge |
IRC_CLOAK_KEY_1/2/3 | All IRC servers in the network |
IRC_SERVICES_PASSWORD:
Docker network isolation
All services run on a single Docker bridge network namedatl-chat, defined in infra/compose/networks.yaml. This means every container can reach every other container by hostname on internal ports, but only explicitly published ports are accessible from the host.
Externally exposed ports
These ports are bound to the host and accessible to external clients. In production, they bind to the Tailscale IP (ATL_CHAT_IP). In development, they bind to 127.0.0.1 via .env.dev.
| Service | Port | Variable | Purpose |
|---|---|---|---|
| UnrealIRCd | 6697 | IRC_TLS_PORT | IRC client connections (TLS) |
| UnrealIRCd | 6900 | IRC_SERVER_PORT | Server-to-server linking |
| UnrealIRCd | 8600 | IRC_RPC_PORT | JSON-RPC API |
| UnrealIRCd | 8000 | IRC_WEBSOCKET_PORT | WebSocket for web clients |
| UnrealIRCd | 8081 | ATHEME_HTTPD_PORT | Atheme JSON-RPC (shared network namespace) |
| WebPanel | 8080 | WEBPANEL_PORT | Admin web interface |
| Prosody | 5222 | PROSODY_C2S_PORT | XMPP client-to-server |
| Prosody | 5269 | PROSODY_S2S_PORT | XMPP server-to-server |
| Prosody | 5223 | PROSODY_C2S_DIRECT_TLS_PORT | XMPP Direct TLS |
| Prosody | 5270 | PROSODY_S2S_DIRECT_TLS_PORT | XMPP S2S Direct TLS |
| Prosody | 5280 | PROSODY_HTTP_PORT | BOSH / WebSocket |
| Prosody | 5000 | PROSODY_PROXY65_PORT | File transfer proxy |
| Nginx (XMPP) | 5281 | PROSODY_HTTPS_PORT | XMPP HTTPS proxy |
| The Lounge | 9000 | THELOUNGE_PORT | Web IRC client |
| Dozzle | 8082 | DOZZLE_PORT | Log viewer (dev profile only) |
Internal-only communication
These connections happen over theatl-chat Docker network and are never exposed to the host:
| From | To | Port | Purpose |
|---|---|---|---|
| Atheme | UnrealIRCd | 6901 | Services link (shared network namespace via network_mode: service:atl-irc-server) |
| Bridge | UnrealIRCd | 6697 | IRC connection |
| Bridge | Prosody | 5347 | XMPP component protocol |
| The Lounge | UnrealIRCd | 6697 | IRC connection (WebIRC) |
| WebPanel | UnrealIRCd | 8600 | JSON-RPC |
| cert-manager | — | — | Writes to shared data/certs/ volume only |
Atheme network namespace sharing
Atheme usesnetwork_mode: service:atl-irc-server, which means it shares UnrealIRCd’s network stack entirely. Atheme connects to UnrealIRCd on 127.0.0.1:6901 (loopback within the shared namespace). This is why the Atheme HTTP port (8081) appears as a published port on the UnrealIRCd container.
Production hardening
In production, bind published ports to your Tailscale IP rather than0.0.0.0:
TLS configuration best practices
Detailed certificate management is covered in the SSL/TLS page. This section tore private keys in the repository. Thedata/ directory is in .gitignore.
- In development,
just initgenerates self-signed certificates. These are not suitable for production.
Post-renewal hooks
After certificate renewal, services need to pick up the new certificates:STS configuration
UnrealIRCd’s Strict Transport Security is configured via environment variables:| Variable | Default | Description |
|---|---|---|
IRC_STS_DURATION | 1m | STS policy duration (use longer values like 365d in production) |
IRC_STS_PRELOAD | no | Whether to include in STS preload lists |
Gitleaks pre-commit hook
The repository uses Gitleaks as a pre-commit hook to prevent accidental secret commits. It is configured in.pre-commit-config.yaml:
Setup
Install the pre-commit hooks:How it works
Gitleaks scans staged files against a comprehensive rule set defined in.gitleaks.toml (auto-generated from the upstream Gitleaks default config). It detects patterns for hundreds of secret types: API keys, tokens, passwords, private keys, and more.
The .gitleaks.toml global allowlist excludes common false positives:
- Binary and font files
- Lock files (
pnpm-lock.yaml,package-lock.json, etc.) - Vendored dependencies
- Template variables (
${VAR},$VAR,{{ var }}) - Common file paths
Handling a blocked commit
If Gitleaks blocks your commit, it means a potential secret was detected in your staged changes:- Review the Gitleaks output to identify the flagged file and line
- If it is a real secret:
- Remove the secret from the file
- Move it to
.env(which is gitignored) - Reference it via environment variable substitution
- If it is a false positive:
- Add an inline comment
# gitleaks:allowon the flagged line, or - Add the specific pattern to the
[[rules.allowlists]]section in.gitleaks.toml
- Add an inline comment
Handling accidental secret commits
If a secret was committed before the hook was installed:- Rotate the compromised secret immediately using the generation methods above
-
Remove the secret from the repository history using
git filter-repo: -
Force-push the cleaned history:
- Notify all contributors to re-clone or rebase onto the cleaned history
- If the secret was for an external service (Discord token, Cloudflare API token), revoke it at the provider and generate a new one
Warning: Force-pushing rewrites history for all collaborators. Coordinate with your team before doing this. In most cases, rotating the secret is sufficient — history rewriting is only necessary if the secret grants persistent access that cannot be revoked.
Verification checklist
After configuring secrets for a new deployment, verify everything is in place:Related pages
- SSL/TLS — certificate management, renewal, and per-service cert paths
- Deployment — production deployment runbook
- Environment Variables — complete variable reference with security warnings
- Architecture — system design and service connectivity
- IRC Configuration — UnrealIRCd secrets, oper blocks, and cloak keys
- XMPP Configuration — Prosody TLS and authentication settings
- Atheme Configuration — Atheme services link password and SASL setup
- Bridge Configuration — bridge credentials and Discord token