- domain_base changed to walava.io
- domain_n8n now auto.walava.io
- Added domain_landing for walava.io root
- Added walava-web landing page container + Traefik route
- Updated Cloudflare token/zone_id for walava.io account
- Updated ACME email to walava@tutamail.com
- Fixed discord-bot image to use domain_base variable
- DNS records already created in Cloudflare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ensures removed services (vaultwarden, mailserver, snappymail)
are automatically stopped on next deploy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents the 'meta json: readObjectStart' error on fresh deploys.
Existing hooks already fixed via direct DB update.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add vault_forgejo_api_token (Personal Access Token with write:repository)
- Ansible task now creates Discord webhook on both jack/infra and jack/discord-bot
- Webhooks already created manually for this deploy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Container was on backend (internal: true) only — couldn't resolve
discord.com for webhook notifications. Added proxy network which
has outbound internet access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Leftover after Vaultwarden removal caused CI/CD deploy to fail with
'vaultwarden_admin_token is undefined' during .env template rendering.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without these env vars Next.js SSR renders with wrong base URL causing
React hydration error #418 — server/client HTML mismatch on first render.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SnappyMail defaulted to localhost:143 for IMAP. Create csrx.ru.json
domain config pointing to the mailserver container (shared front network):
- IMAP: mailserver:993 SSL
- SMTP: mailserver:587 STARTTLS with auth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
djmaze/snappymail does not reliably apply SNAPPYMAIL_ADMIN_PASSWORD.
Instead: read current hash from application.ini, verify it against vault
password using password_verify() in container PHP, update only if wrong.
Idempotent — no restart if password is already correct.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace broken PHP require path with docker restart to let entrypoint
apply SNAPPYMAIL_ADMIN_PASSWORD env var (path /var/www/snappymail/index.php
does not exist in djmaze/snappymail image)
- Move snappymail from webmail-internal to mail-internal so it can reach
mailserver for IMAP/SMTP connections
- Remove unused webmail-internal network
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The SNAPPYMAIL_ADMIN_PASSWORD env var may not apply if the container
started when data dir had wrong permissions. Now sets password directly
via RainLoop PHP API after every deploy — idempotent and reliable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Timeweb S3 doesn't support per-object storage class via API parameter.
Cold storage is configured at bucket level in Timeweb control panel.
Also: make S3 upload failures explicit (exit 1) instead of silently ignored.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SnappyMail container runs as www-data (uid 82 in Alpine).
Directory was created as deploy:deploy (uid 1000) → [202] is_readable() error.
Fix: chown 82:82 on the data directory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change cron from daily 03:00 to every hour (minute=0)
- Change S3 path from main/ to data/ as requested
- Change storage class from STANDARD to COLD (Timeweb cold storage)
- Update S3 pruning to match new data/ prefix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace fragile file-content lookup with proper failed_when that accepts
'already exists' exit code 1 as a non-failure. Simpler and works on every run.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lookup(..., errors='ignore') returns None (not empty string) for missing files.
Use | default('', true) to also convert falsy None to empty string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add docker-mailserver (Postfix+Dovecot) with SSL via certbot+Cloudflare DNS-01
- Add SnappyMail webmail client at webmail.csrx.ru (port 8888)
- Open UFW ports 25/465/587/993 on tools server
- Create mail accounts: noreply@, admin@, jack@csrx.ru
- Generate DKIM key and print DNS instructions on first run
- Add Traefik route on main server proxying webmail → tools:8888
- Add all secrets to vault (mailserver passwords, snappymail admin)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mail-internal is internal:true (no internet). mailserver needs the
front network to resolve DNS and deliver emails to external MX servers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker-mailserver uses opendkim by default; generated keys go to
config/opendkim/keys/<domain>/mail.{private,txt}, not rspamd/dkim/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
setup email list fails with rc=1 when postfix-accounts.cf doesn't
exist yet (fresh install). Check the mounted config file on the host
instead, which correctly handles the empty/missing case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds docker-mailserver (SMTP_ONLY mode) to the tools stack so Outline
can send magic-link emails without depending on an external SMTP provider.
Changes:
- docker-compose.yml.j2: add mailserver service + mail-internal network
outline gets mail-internal network to reach mailserver
- env.j2: point Outline SMTP at local mailserver:587 with noreply account
- defaults/main.yml: add mailserver_image (v14)
- tasks/main.yml: create mailserver dirs, wait for postfix ready,
idempotent account creation, DKIM key generation + DNS instructions
- inventory/group_vars/all/main.yml: add mailserver_noreply_password alias
- vault.yml: add vault_mailserver_noreply_password
After deploy, Ansible will print DKIM/SPF/DMARC DNS records to add
to Cloudflare.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backup (backup.sh.j2):
- Creates a single data_YYYY-MM-DD_HH-MM.tar.gz archive
- Unified data/ layout: databases/ (pg_dump .sql.gz) + volumes/ (docker volumes)
- Includes RESTORE.md with step-by-step instructions inside the archive
- S3 uploads to main/ prefix instead of flat root
Outline (tools role):
- Add SMTP_HOST/PORT/FROM vars to env.j2 template (required for email magic-link auth to activate)
- Add outline_smtp_* defaults to roles/tools/defaults/main.yml
- Without SMTP_HOST, the email auth plugin is disabled and clicking login does nothing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Docker cannot mount to /var/log/traefik when /var/log is already
bind-mounted (read-only). The nested mount fails with 'read-only
file system' error in the overlay upper layer.
The mount was unused anyway — promtail config only reads syslog,
auth.log, and Docker container logs via the socket.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
crowdsecurity/cs-firewall-bouncer:v0.0.31 does not exist on Docker Hub.
The bouncer service was already removed from docker-compose.yml.
Remove from pre-pull list and defaults to unblock CI/CD deploy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Docker 29.x does not create DNAT rules for containers only on internal
networks. Add a non-internal 'front' network that outline and n8n join
alongside their internal networks, enabling host port binding to work.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add traefik-auth filter: ban IPs with 10+ HTTP 401/403 in 5 min
- Add forgejo-ssh jail: ban after 3 failed SSH attempts (24h ban)
- Both jails are active; forgejo-ssh already detected 8 real attempts
- Traefik access.log now written to /opt/services/traefik/logs/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch Traefik ACME to dnsChallenge (provider: cloudflare)
- Add *.csrx.ru wildcard cert via tls.stores.default.defaultGeneratedCert
- Pass CLOUDFLARE_DNS_API_TOKEN to Traefik via env_file: .env
- Add Cloudflare IP ranges to forwardedHeaders.trustedIPs (real visitor IPs)
- Fix UFW: allow 172.16.0.0/12 on 80/443 so act_runner can reach Forgejo
- Add A records: auth.csrx.ru, status.csrx.ru, csrx.ru root → 87.249.49.32
Result: one *.csrx.ru cert covers all subdomains, auto-renewed by Traefik.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DNS-01 + wildcard cert requires Cloudflare to be authoritative NS.
Until propagation completes, use httpChallenge on port 80.
Plan after Cloudflare NS is active:
1. Switch back to dnsChallenge in traefik.yml.j2
2. Re-enable tls.stores.default.defaultGeneratedCert in routes.yml.j2
3. Clear acme.json → Traefik issues *.csrx.ru wildcard cert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add tls.stores.default.defaultGeneratedCert in dynamic config:
- Traefik requests one *.csrx.ru + csrx.ru SAN cert via DNS-01
- All existing and future subdomains use this single cert
- No per-service cert issuance wait when adding new services
- Cert auto-renewed by Traefik ~30 days before expiry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
firewall.yml:
- Allow 172.16.0.0/12 and 10.0.0.0/8 on ports 80/443 so act_runner
job containers can reach git.csrx.ru (Forgejo via Traefik)
- Without this, Cloudflare-only rules broke CI/CD pipeline
unattended_upgrades.yml (new):
- Install unattended-upgrades + apt-listchanges
- Configure auto-apply of security patches only (not all updates)
- Auto-clean every 7 days, remove unused deps
- No auto-reboot (manual control over kernel reboots)
base/tasks/main.yml:
- Add unattended_upgrades.yml to task sequence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Traefik traefik.yml.j2:
- Add forwardedHeaders.trustedIPs with all Cloudflare CIDR ranges
on both web and websecure entrypoints so rate limiting and
CrowdSec see real visitor IPs, not Cloudflare proxy IPs
firewall.yml:
- Replace open HTTP/HTTPS rules with per-CIDR allow rules
scoped to Cloudflare IP ranges only
- Direct access to ports 80/443 bypassing Cloudflare is now blocked
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cannot use comparison operators inside label matchers {}.
Move the > 0 filter outside braces as a scalar filter on the
denominator — idiomatic Prometheus way to exclude unlimited containers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add vault_s3_access_key / vault_s3_secret_key to Ansible Vault
- Expose via s3_access_key / s3_secret_key in all/main.yml
- Add s3_endpoint + s3_bucket to backup role defaults
- Install awscli via apt in backup role tasks
- Extend backup.sh.j2: upload *.gz to S3 after local backup,
prune S3 objects older than backup_retention_days
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>