Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.allthingslinux.org/llms.txt

Use this file to discover all available pages before exploring further.

This page covers how to generate, store, and rotate secrets for every atl.chat service, how the Docker network isolates internal traffic from external access, TLS hardening guidance, and how the Gitleaks pre-commit hook prevents accidental secret commits.

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:
# Restrict .env to owner-only read/write
chmod 600 .env

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:
# Generates 3 cloak keys and writes them to .env automatically
just irc gencloak
This runs 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:
# Requires the IRC container to be running
just irc generate-password
This prompts you for a password (input is hidden), then outputs an Argon2 hash. Copy the hash into your .env:
IRC_OPER_PASSWORD="$argon2id$v=19$m=6144,t=2,p=2$..."
After updating, apply the change:
# Re-generate config from templates
bash scripts/prepare-config.sh

# Reload without restart (as IRCOp: /REHASH) or restart the container
docker compose restart atl-irc-server

IRC die/restart password (IRC_DRPASS)

This password protects the /DIE and /RESTART IRC commands. Generate a random value:
openssl rand -base64 32
Set it in .env:
IRC_DRPASS=<generated-value>

IRC services password (IRC_SERVICES_PASSWORD)

Shared secret between UnrealIRCd and Atheme for the services link. Generate a strong random value:
openssl rand -base64 32

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:
# For the web frontend
openssl rand -base64 32
# For The Lounge
openssl rand -base64 32

WebPanel RPC password (WEBPANEL_RPC_PASSWORD)

Authenticates the WebPanel to UnrealIRCd’s JSON-RPC interface:
openssl rand -base64 32

Bridge secrets

VariablePurposeGeneration
BRIDGE_DISCORD_TOKENDiscord bot tokenCreate at Discord Developer Portal → Bot → Reset Token
BRIDGE_PORTAL_TOKENPortal API authenticationopenssl rand -base64 32
BRIDGE_XMPP_COMPONENT_SECRETXMPP component authenticationopenssl rand -base64 32
BRIDGE_IRC_OPER_PASSWORDBridge IRC oper credentialsopenssl rand -base64 32

Prosody (XMPP) secrets

VariablePurposeGeneration
PROSODY_DB_PASSWORDPostgreSQL database password (production only)openssl rand -base64 32
PROSODY_OAUTH2_REGISTRATION_KEYOAuth2 client registration keyopenssl rand -base64 32

TURN secret (TURN_SECRET)

Shared secret for TURN/STUN authentication:
openssl rand -base64 32

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:
openssl rand -base64 32

Portal integration passwords

These variables are consumed by the external ATL Portal service, not by this monorepo’s compose stack:
VariablePurposeGeneration
IRC_UNREAL_RPC_PASSWORDPortal → UnrealIRCd JSON-RPCShould match WEBPANEL_RPC_PASSWORD
PROSODY_REST_PASSWORDPortal → Prosody REST APIopenssl rand -base64 32

Credential rotation

Rotate all secrets on a regular schedule (every 6–12 months) or immediately after a suspected compromise.

Rotation procedure

  1. Generate a new value using the methods above
  2. Update .env with the new value
  3. Re-generate config templates:
    bash scripts/prepare-config.sh
    
  4. Restart affected services:
    # Single service
    docker compose restart atl-irc-server
    
    # Full stack
    docker compose down
    docker compose up -d
    
  5. Verify the service is healthy:
    docker compose ps
    

Coordinated rotation

Some secrets are shared between services and must be updated in lockstep:
SecretServices that share it
IRC_SERVICES_PASSWORDUnrealIRCd ↔ Atheme
ATL_WEBIRC_PASSWORDUnrealIRCd ↔ Next.js web app
THELOUNGE_WEBIRC_PASSWORDUnrealIRCd ↔ The Lounge
WEBPANEL_RPC_PASSWORDUnrealIRCd ↔ WebPanel
BRIDGE_XMPP_COMPONENT_SECRETProsody ↔ Bridge
BRIDGE_IRC_OPER_PASSWORDUnrealIRCd ↔ Bridge
IRC_CLOAK_KEY_1/2/3All IRC servers in the network
After updating a shared secret, restart both services that use it. For example, after rotating IRC_SERVICES_PASSWORD:
bash scripts/prepare-config.sh
docker compose restart atl-irc-server
# Atheme shares UnrealIRCd's network namespace and restarts with it,
# but if running independently:
docker compose restart atl-irc-services

Docker network isolation

All services run on a single Docker bridge network named atl-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.
ServicePortVariablePurpose
UnrealIRCd6697IRC_TLS_PORTIRC client connections (TLS)
UnrealIRCd6900IRC_SERVER_PORTServer-to-server linking
UnrealIRCd8600IRC_RPC_PORTJSON-RPC API
UnrealIRCd8000IRC_WEBSOCKET_PORTWebSocket for web clients
UnrealIRCd8081ATHEME_HTTPD_PORTAtheme JSON-RPC (shared network namespace)
WebPanel8080WEBPANEL_PORTAdmin web interface
Prosody5222PROSODY_C2S_PORTXMPP client-to-server
Prosody5269PROSODY_S2S_PORTXMPP server-to-server
Prosody5223PROSODY_C2S_DIRECT_TLS_PORTXMPP Direct TLS
Prosody5270PROSODY_S2S_DIRECT_TLS_PORTXMPP S2S Direct TLS
Prosody5280PROSODY_HTTP_PORTBOSH / WebSocket
Prosody5000PROSODY_PROXY65_PORTFile transfer proxy
Nginx (XMPP)5281PROSODY_HTTPS_PORTXMPP HTTPS proxy
The Lounge9000THELOUNGE_PORTWeb IRC client
Dozzle8082DOZZLE_PORTLog viewer (dev profile only)

Internal-only communication

These connections happen over the atl-chat Docker network and are never exposed to the host:
FromToPortPurpose
AthemeUnrealIRCd6901Services link (shared network namespace via network_mode: service:atl-irc-server)
BridgeUnrealIRCd6697IRC connection
BridgeProsody5347XMPP component protocol
The LoungeUnrealIRCd6697IRC connection (WebIRC)
WebPanelUnrealIRCd8600JSON-RPC
cert-managerWrites to shared data/certs/ volume only

Atheme network namespace sharing

Atheme uses network_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 than 0.0.0.0:
# .env (production)
ATL_CHAT_IP=100.64.7.0
ATL_GATEWAY_IP=100.64.1.0
This ensures services are only reachable over the Tailscale mesh, not on the public internet. Place a reverse proxy (e.g. Caddy, nginx) in front of web-facing ports (8000, 8080, 9000, 5280, 5281) to add rate limiting and additional TLS termination.

TLS configuration best practices

Detailed certificate management is covered in the SSL/TLS page. This section tore private keys in the repository. The data/ directory is in .gitignore.
  • In development, just init generates self-signed certificates. These are not suitable for production.

Post-renewal hooks

After certificate renewal, services need to pick up the new certificates:
# UnrealIRCd: reload config (picks up new certs)
docker compose kill -s HUP atl-irc-server

# Prosody: reload
docker compose exec atl-xmpp-server prosodyctl reload

STS configuration

UnrealIRCd’s Strict Transport Security is configured via environment variables:
VariableDefaultDescription
IRC_STS_DURATION1mSTS policy duration (use longer values like 365d in production)
IRC_STS_PRELOADnoWhether 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:
- repo: https://github.com/gitleaks/gitleaks
  rev: v8.30.0
  hooks:
    - id: gitleaks
      args: [--config, .gitleaks.toml]

Setup

Install the pre-commit hooks:
# If core.hooksPath is set by your environment, unset it first
git config --unset-all core.hooksPath

# Install hooks
pre-commit install
Or run manually:
# Scan all files
pre-commit run gitleaks --all-files

# Equivalent via just
just lint

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:
  1. Review the Gitleaks output to identify the flagged file and line
  2. 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
  3. If it is a false positive:
    • Add an inline comment # gitleaks:allow on the flagged line, or
    • Add the specific pattern to the [[rules.allowlists]] section in .gitleaks.toml

Handling accidental secret commits

If a secret was committed before the hook was installed:
  1. Rotate the compromised secret immediately using the generation methods above
  2. Remove the secret from the repository history using git filter-repo:
    # Install git-filter-repo if needed
    pip install git-filter-repo
    
    # Remove the file containing the secret from all history
    git filter-repo --invert-paths --path <file-with-secret>
    
    # Or replace a specific string across all history
    git filter-repo --replace-text <(echo 'OLD_SECRET==>REDACTED')
    
  3. Force-push the cleaned history:
    git push --force --all
    git push --force --tags
    
  4. Notify all contributors to re-clone or rebase onto the cleaned history
  5. 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:
# Check .env exists and has restricted permissions
ls -la .env
# Expected: -rw------- 1 user user ... .env

# Verify all placeholder values have been replaced
grep -c 'change_me' .env
# Expected: 0

# Verify Gitleaks hook is installed
pre-commit run gitleaks --all-files
# Expected: Passed (or specific findings to address)

# Verify services start with the new secrets
docker compose up -d
docker compose ps
# Expected: all services show "Up" / "healthy"