diff --git a/DISCOURSE_RECOVERY.md b/DISCOURSE_RECOVERY.md new file mode 100644 index 0000000..e3ffb70 --- /dev/null +++ b/DISCOURSE_RECOVERY.md @@ -0,0 +1,139 @@ +# Discourse State and Database Recovery + +This restores Discourse from the BorgBase backup configured in `discourse.nix`. +It replaces the current Discourse state directory and PostgreSQL database with +the selected Borg archive. + +The backup contains: + +- `/var/lib/discourse` +- `/var/backup/discourse/discourse.dump` + +## Requirements + +- BorgBase repo URL: + `ssh://oas17j8p@oas17j8p.repo.borgbase.com/./repo` +- Decrypted Borg SSH private key from the flash drive +- Borg repository passphrase from the flash drive +- A target server that has already been switched to this NixOS config +- A shell with `borg`, `openssh`, `postgresql`, and `rsync` + +On NixOS or another machine with Nix: + +```sh +nix-shell -p borgbackup openssh postgresql rsync +``` + +## Prepare Borg Access + +Copy the Borg SSH key into a local recovery directory: + +```sh +mkdir -p ~/borg-recovery/discourse +cp /path/to/flash/borg-private-key ~/borg-recovery/discourse/borg-private-key +chmod 600 ~/borg-recovery/discourse/borg-private-key +``` + +Set the Borg connection environment: + +```sh +export BORG_REPO='ssh://oas17j8p@oas17j8p.repo.borgbase.com/./repo' +export BORG_RSH='ssh -i ~/borg-recovery/discourse/borg-private-key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new' +``` + +Read the Borg passphrase without showing it on screen: + +```sh +read -rsp 'Borg passphrase: ' BORG_PASSPHRASE +export BORG_PASSPHRASE +echo +``` + +## Extract a Backup + +List available archives: + +```sh +borg list +``` + +Choose an archive name from the list, then extract only the Discourse state and +database dump into a temporary directory. Do not extract directly into `/`. + +```sh +export ARCHIVE='ARCHIVE_NAME_FROM_BORG_LIST' + +mkdir -p ~/borg-recovery/discourse/extract +cd ~/borg-recovery/discourse/extract + +borg extract ::$ARCHIVE var/lib/discourse var/backup/discourse/discourse.dump +``` + +## Restore on the Server + +Run these commands from `~/borg-recovery/discourse/extract` on the target +server. They assume the NixOS config has already created the `discourse` user, +`postgresql.service`, and `discourse-postgresql.service`. + +Capture absolute tool paths so they still work through `sudo`: + +```sh +RSYNC="$(command -v rsync)" +PSQL="$(command -v psql)" +DROPDB="$(command -v dropdb)" +PG_RESTORE="$(command -v pg_restore)" +``` + +Stop Discourse and make sure PostgreSQL is running: + +```sh +sudo systemctl stop discourse.service +sudo systemctl start postgresql.service +``` + +Restore `/var/lib/discourse`: + +```sh +sudo "$RSYNC" -a --delete ./var/lib/discourse/ /var/lib/discourse/ +sudo chown -R discourse:discourse /var/lib/discourse +``` + +Replace the `discourse` PostgreSQL database: + +```sh +sudo -u postgres "$PSQL" -d postgres -v ON_ERROR_STOP=1 \ + -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'discourse';" + +sudo -u postgres "$DROPDB" --if-exists discourse +sudo systemctl restart discourse-postgresql.service + +sudo -u postgres "$PG_RESTORE" \ + --exit-on-error \ + --no-owner \ + --role=discourse \ + --dbname=discourse \ + ./var/backup/discourse/discourse.dump + +sudo systemctl restart discourse-postgresql.service +``` + +Start Discourse: + +```sh +sudo systemctl start discourse.service +``` + +## Verify + +Check the service and recent logs: + +```sh +sudo systemctl status discourse.service --no-pager +sudo journalctl -u discourse.service -b --no-pager -n 100 +``` + +If networking and DNS are already restored, open: + +```text +https://discourse.torahimderecheretz.com +``` diff --git a/discourse.nix b/discourse.nix new file mode 100644 index 0000000..d51b58f --- /dev/null +++ b/discourse.nix @@ -0,0 +1,131 @@ +{ + config, + lib, + pkgs, + ... +}: +{ + services.discourse = { + enable = true; + admin = { + email = "yisroel.d.baum@gmail.com"; + fullName = "Yisroel Baum"; + passwordFile = config.sops.secrets."discourse-admin-password".path; + username = "yisroeldbaum"; + }; + database.ignorePostgresqlVersion = true; + hostname = "discourse.torahimderecheretz.com"; + mail = { + notificationEmailAddress = "system@torahimderecheretz.com"; + contactEmailAddress = "system@torahimderecheretz.com"; + + outgoing = { + serverAddress = "in-v3.mailjet.com"; + port = 587; + username = "1ebdd73d944240c1a9c3201e52a1e8c6"; + passwordFile = config.sops.secrets."discourse-mail-key".path; + authentication = "login"; + }; + }; + secretKeyBaseFile = config.sops.secrets."discourse-secret-key".path; + siteSettings = { + required = { + title = "Torah Im Derech Eretz"; + site_description = "A forum to discuss the ideas of Rav Shamshon Refael Hirsch"; + }; + }; + }; + + services.borgbackup.jobs.discourse = { + paths = [ + "/var/lib/discourse" + "/var/backup/discourse/discourse.dump" + ]; + repo = "ssh://oas17j8p@oas17j8p.repo.borgbase.com/./repo"; + user = "root"; + group = "root"; + encryption = { + mode = "repokey-blake2"; + passCommand = "${pkgs.coreutils}/bin/cat ${config.sops.secrets."borg-passphrase".path}"; + }; + doInit = true; + compression = "auto,zstd"; + startAt = "*-*-* 07:15:00"; + persistentTimer = true; + prune.keep = { + daily = 7; + weekly = 4; + monthly = 6; + }; + readWritePaths = [ "/var/backup/discourse" ]; + environment.BORG_RSH = "ssh -i ${config.sops.secrets."borg-private-key".path} -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.config/borg/known_hosts"; + preHook = '' + discourseWasActive=/root/.cache/borg/discourse-was-active + dumpFile=/var/backup/discourse/discourse.dump + + if ${pkgs.systemd}/bin/systemctl is-active --quiet discourse.service; then + touch "$discourseWasActive" + ${pkgs.systemd}/bin/systemctl stop discourse.service + else + rm -f "$discourseWasActive" + fi + + rm -f "$dumpFile" + umask 077 + ${lib.getExe' pkgs.su "su"} -s ${pkgs.runtimeShell} ${config.services.postgresql.superUser} -c '${config.services.postgresql.package}/bin/pg_dump --format=custom --dbname=${config.services.discourse.database.name}' > "$dumpFile" + ''; + postHook = '' + discourseWasActive=/root/.cache/borg/discourse-was-active + dumpFile=/var/backup/discourse/discourse.dump + + rm -f "$dumpFile" + + if [ -e "$discourseWasActive" ]; then + ${pkgs.systemd}/bin/systemctl start discourse.service + rm -f "$discourseWasActive" + fi + ''; + }; + + systemd.tmpfiles.rules = [ + "d /var/backup/discourse 0700 root root - -" + ]; + + systemd.services.borgbackup-job-discourse = { + after = [ + "sops-install-secrets.service" + "network-online.target" + "postgresql.target" + "discourse-postgresql.service" + ]; + wants = [ + "network-online.target" + "postgresql.target" + "discourse-postgresql.service" + ]; + }; + + sops.secrets = { + "discourse-admin-password" = { + sopsFile = ./secrets/discourseAdminPassword.yaml; + mode = "0400"; + key = "password"; + owner = "discourse"; + group = "discourse"; + }; + "discourse-secret-key" = { + sopsFile = ./secrets/discourseSecretKey.yaml; + mode = "0400"; + key = "key"; + owner = "discourse"; + group = "discourse"; + }; + "discourse-mail-key" = { + sopsFile = ./secrets/discourseMailKey.yaml; + mode = "0400"; + key = "key"; + owner = "discourse"; + group = "discourse"; + }; + }; +} diff --git a/flake.lock b/flake.lock index bdd385d..cb1d765 100644 --- a/flake.lock +++ b/flake.lock @@ -122,18 +122,34 @@ "type": "github" } }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "tide": { "inputs": { "nixpkgs": [ "nixpkgs" - ] + ], + "utils": "utils_2" }, "locked": { - "lastModified": 1784446675, - "narHash": "sha256-r7X6qZeRLdOskfw3Ktz9VbatkHu/4Tb0wjpYx8AVZdc=", + "lastModified": 1778485449, + "narHash": "sha256-Y8p1JKfU7I1T33qI7F6zXobtQqLqry4NVBb2eAlnBqc=", "ref": "refs/heads/master", - "rev": "21cca99e9e06f7aec0d14ba782cecad5e56ab098", - "revCount": 140, + "rev": "ae9dd787a6ac64c300251cf047274dd76af6d2d6", + "revCount": 137, "type": "git", "url": "https://git.yisroelbaum.com/yisroelbaum/TIDE" }, @@ -159,6 +175,24 @@ "repo": "flake-utils", "type": "github" } + }, + "utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 08739f7..3921e65 100644 --- a/flake.nix +++ b/flake.nix @@ -45,6 +45,7 @@ nixosConfigurations.nixos = nixpkgs.lib.nixosSystem { modules = [ ./configuration.nix + ./discourse.nix ./tide.nix ./rabbi-gerzi.nix home-manager.nixosModules.home-manager diff --git a/secrets/README.md b/secrets/README.md index b4eb134..5350e1a 100644 --- a/secrets/README.md +++ b/secrets/README.md @@ -15,14 +15,12 @@ host's age key. ``` sudo grep "public key" /var/lib/sops-nix/key.txt ``` -3. On a workstation, put that public key into `.sops.yaml` at the repository - root and encrypt the required secret files. +3. On a workstation, put that public key into `.sops.yaml` at + the repo root and encrypt `tide.yaml.example` into + `tide.yaml`. -## TIDE - -Create `tide.yaml` from `tide.yaml.example` and encrypt it with sops. It must -contain `admin-password`, `mail-key`, and `secret-key`. The encrypted file is -committed; the example contains placeholders only. +`tide.yaml` is encrypted and committed. `tide.yaml.example` is +the plaintext template. ## Rabbi Gerzi diff --git a/secrets/discourseAdminPassword.yaml b/secrets/discourseAdminPassword.yaml new file mode 100644 index 0000000..398e0ef --- /dev/null +++ b/secrets/discourseAdminPassword.yaml @@ -0,0 +1,16 @@ +password: ENC[AES256_GCM,data:6/a9U7I2joMSkfiysYgMrBN3AE3DRUjQsZw=,iv:EDeN/IClY/yNkVi/FLiTeeOjkj5Df/1l4XXzf5STot0=,tag:8uoXPXJuL4JlMGzgtK6D+w==,type:str] +sops: + age: + - recipient: age1haj8v88kjna6ttkdufjpyjcf478kyvclnpdc8jwh97ewhqcc9eqsgrku4v + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvYWtUZ2JYU3VNaHQ1bXVL + V0ZmRVIxVDg2SE5GcU5UMTZjcnVOUlBZOHlzCjc5elV3bk54ak05WStvNGUvNCtP + aVNJT0tFTVFMb2NKTlpValJ6YWFVcEEKLS0tIDlEUjRncWFsUGptZTBmSm1idm0z + ZUgxUHhvejMreHUrbDVwTkxwOE1DSkUK62qiTlgQvKruDj3Bh/8nJH+5CBl+fzbJ + q5ryzSfTpEcSfBwRENqIOYMOTjzr+y+JGymjokc9rCYb0NPQUFpHzg== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-06-02T18:00:59Z" + mac: ENC[AES256_GCM,data:H/+voyLLpsGxgTNjN96PL+I1tpW8Ylo+mWVTbsaUMnnOZIkfA81MzEjtdWOs0Pg4AysAeMbmJUob+gTtSfcqnVvhv/FBVHuPji+Vz0cj0TM8k7oLlaIgeL/MAZ1mhrxDIUK5pQYSVnaaWrQ43Z4DeqaboDY7feUOp8n6emICo5I=,iv:dCPOGBC80ZtM0ObeRRMAGH2f4K5zFtckaxKAJZll8yI=,tag:xwBJzExJXAw0T39hPdmgRw==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.2 diff --git a/secrets/discourseMailKey.yaml b/secrets/discourseMailKey.yaml new file mode 100644 index 0000000..ff58b3b --- /dev/null +++ b/secrets/discourseMailKey.yaml @@ -0,0 +1,16 @@ +key: ENC[AES256_GCM,data:Flvbs7sRCQ3BtSJXWurmVA+QQmvSo/fxrOVJNalBjj8=,iv:ddmk1XCjvNRmmqGhZwOWDlYSkdKz3ryyQwrav/T6Oz8=,tag:DvSjwdK01UAvBL1mYCe7kA==,type:str] +sops: + age: + - recipient: age1haj8v88kjna6ttkdufjpyjcf478kyvclnpdc8jwh97ewhqcc9eqsgrku4v + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVUGFmM010b3NhdCtPME5O + RWRkUjF0akVEZVFtNkhZeUVSeWpWL3pKNG1nCllObGlPOTcrSyt1a2NCc0U4TVZM + cWVQbjJocEUySHFYaDFmNzllOGhuQWcKLS0tIHFJeFdUWENxK1RRSlhvSEI4NXlO + TG1wM1FTWjBBWHdHNnBUUE1kNWE4akUK1Ehv5kYNZrbf3m3vH5Oyy/g2mz8kIX8T + l2k1LbhUo3cNzIjkNM05RSrLZEBPItoo2nc9oTAPZiF1U/K76SMP6g== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-06-03T07:59:50Z" + mac: ENC[AES256_GCM,data:E4WwhOqUCmOBXwYUR5nFzth59F50KE7k88NkOBc6ERnX4b5joy0lmExenprH019UVJ/HSXOCWphUMwTat70uzwhaKBtlfa7AWmWyMa6zhIzxTsFagI4h1nWQP00uGb0MM3ULR3mcLtxXyTtZFWuY/d8ttdYBuv2oe29ikJTX610=,iv:gAZPLiYcNGq8LSEpIxDzDqdan+CJKyQ+SnQXHmponns=,tag:3tI0it3h6foT9W7zK3rmxg==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.2 diff --git a/secrets/discourseSecretKey.yaml b/secrets/discourseSecretKey.yaml new file mode 100644 index 0000000..0265ff6 --- /dev/null +++ b/secrets/discourseSecretKey.yaml @@ -0,0 +1,16 @@ +key: ENC[AES256_GCM,data:lZXbXrorH/GGGg93WgnG6GlzNs0NEy0BlForhgKlwllsf2uJWnBukQDDxxI6gUWMp+Uzc3e9plrmGbM7HQfsccDoQux7kJjGdt5CJgou0eh4J8WGw51M+beixVdF6UuCFFiuLERstzpIP1jhmyDfLZjMVTwfoRe0/YP52Ljgl2Y=,iv:pShAMIXtbEuV7Y2o+3NgTIbPPaoZvx9qF7lj60LMabw=,tag:P6vnrqWzJXXhUl+sJrY2gw==,type:str] +sops: + age: + - recipient: age1haj8v88kjna6ttkdufjpyjcf478kyvclnpdc8jwh97ewhqcc9eqsgrku4v + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlTHF2cGFIQXE2UDJhaVZX + KzhXcWNPbW0zYjBPbTVReUJKOW1ReEt5ZFFNCjFnU25UVVY5eXFyaFFMbkI1NHZ5 + WHRIZ1pYT1ZVWUFJc2hrcGtYTndPdVEKLS0tIHZUSzkxUjEvU0kzRG93Vnc1ZGQz + WGZ3U2dvYmFIVlYvZ3c1Q2JsQkNGek0KGYlQEPskcDN2VxkHskctCTIjPjse1wLL + 6VcEuidu69gkMqvDXR4bmnt8hxSqdH6OS3qEuDfLpqndjfCh//4CDw== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-06-02T18:08:12Z" + mac: ENC[AES256_GCM,data:jYec4f5lr8jYCVvzpM2a3lpIILOTKauz1DNuDIclL7kGA2QXJhW1njXKhmp/VCVbZLfoHCJk6SHyMcxUcx6HXtmc7FAVpu5+Vc/9N+D3gKlyXewDeC41CFdZ7GU0vtoVQK3KQe82rWl+6SRCWXeqLfmRlh7/pLwZy+bj20kG0XY=,iv:sIYEumkyDVrWZRVZpFPtrM96p82rtxW870LRkpHFsPY=,tag:Ey9L21LN+2HjCReuoqJtKQ==,type:str] + unencrypted_suffix: _unencrypted + version: 3.12.2 diff --git a/secrets/tide.yaml b/secrets/tide.yaml index 99818b7..6e57edf 100644 --- a/secrets/tide.yaml +++ b/secrets/tide.yaml @@ -1,18 +1,16 @@ -admin-password: ENC[AES256_GCM,data:yPOLYZ9oMvZrXcfgJQkGL83qPpHrJe7i4Gw=,iv:WmPO6hDNkB7nckFuJjnZX5/8xXpZkUxP8FkjmYmQxzs=,tag:g1dLh009OOBYYL9UkPz3uw==,type:str] -mail-key: ENC[AES256_GCM,data:/9bT8jECSyMqz45LLbWSV15OAJ4wA/ZiCvi9uYrSZK8=,iv:ZN9taTVcIHxqz63pxir7RWTp4CpjVxUBTH+HO92CvPU=,tag:ufVCA36rVBE2ghbI63QXfA==,type:str] -secret-key: ENC[AES256_GCM,data:pFsKF3A9RZSxeV9FwNKIzp+Avu6YWCCHj9zIeex7nHqhuFNuL8D4blQWPujms3wRTUj/OdCbgh56ZXdnEJ6l0vYBKBnP7tS7v7jBhZWtF03SqukCpu+c2HCO6wNvP0tw9zVVSVp0RRmvLq7KhmL6Y9748/TA08MPCVNVP46Xa2E=,iv:yH4vU6kjM+49ssWVAULJcZ9ny264zWsixGs/et6G1Lk=,tag:UWH3Epqn4yGqeTYeBcrDPg==,type:str] +tide-env: ENC[AES256_GCM,data:O4B6jatKVbbVoO53QNgh1ra0Ij9cxo64j+wmmeTZWgX7Us3z0FT3hsOsmYWBsCrKFIGpA2meO4QXlowbJOw43sO7CiCFKuRN7rv58XbGk/hevNhUUTrqU8Seha18ZeoOfmUhD1ScJtyC5RkJb1xFnsFWmFtofebyabhcSLXmJz4UC7Vl2BsVZWULjp9LIkAIOcr2Y6Dm6SoXQa3n6p5yt5rKlY/Xfg/zQoczfi0TG+x+P4SzPPv2U8PJQGG1OqQ0bA82wFNcna+mIb6QIvWz27PeBK4pDfDYnA0SqzKEZsol3WgF5LWPXL5zQpsh11oy7YBWdx8omo3KNBx3+tl2/ACYLuTKRgrxP3UIE38hAm4j95IrL88jFWeNtfZ/fMdTckuQKR2vgbN6Q5HYqnYW72uxCYGZGWD+Jq7MAUduAbvzSlurLoHnKCrA+4Pf8Tu/yJBVDN+urhxnWJy8VpGDXFAGA+reXLtNjZXg1y2KNJOijmzEYfwZ8t53MvCdsJnF562mvCag+DXGex6DZ2qqKeXGa3Lw/YTPBXCHjHJfr+0l91NoOjelA33wS+h+RisHw/iCPlt3NHQgJtCXN7JRWM1Hi3p1ADgovvqJ7yBsT3OXD91QIy5NPSergGsj2vvj+9VBEzSTUuHA4KzYgxrjLUgLeohypr4veFs3qnC3/iTx9gkRfcFX0SMPgj1DWy9+pS42Wi+YDgILAQVSSOgrjhnMv2E0nYNEgZcTPS6SKj+Li4V3/OUVjbMj4N6BLsXxbhyFrKH6MF7ZOXU=,iv:yj0aUCReke2gI3uk/Qer6QBSCQgTwZmCaK7aPBtUwDo=,tag:e6NcaEL3f4EFsaGQKIH+BA==,type:str] sops: age: - - enc: | + - recipient: age1haj8v88kjna6ttkdufjpyjcf478kyvclnpdc8jwh97ewhqcc9eqsgrku4v + enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOZEtYbEhoNW5FVnRlQXIr - TWJaVk5RUmdaUnNKcHRwbElWWCtLQ3l6cm00ClVzNWJZYzRpS21Wckd0dFlEano2 - c1k3TlNZNjU2WGVqUkExblRUZmJpbHcKLS0tIGsvWWhrREoxV2FEREFWQUNpMEZR - MlRTRk13YTljdi9LOW1talBpeDBQdmMKeDvwaLwwYjw1lLFf+/ZRmVGP+chYxa1Y - d/NjatKGm8M/hEKw/MpEKSAMMNAJPUHVwDAeaQTro19TzYerV8e9BQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnQ2RzU0NBUTBhQTBERnBm + RFRPdWlKRWtWMW8zaktGVyt5L3VPTENMakhVCjFsdStSbHFzdmFYcWxuU294VW5Q + MWVQMzlJdmdqTW93Q1MwaVRFaVIvY2sKLS0tIHJxUk96VjRLeWpIanBlNktESDhI + dS9VREordndFSVhnbXFCdUVFN0Z3aG8KN4h2ptJxttY/02FcmRqZa+ujom0LIS11 + cS+qkrk7FnrTbSCNETtjc/FApLTxphptd93zCPJkQtulmo2d9soJlA== -----END AGE ENCRYPTED FILE----- - recipient: age1haj8v88kjna6ttkdufjpyjcf478kyvclnpdc8jwh97ewhqcc9eqsgrku4v - lastmodified: "2026-07-19T07:04:49Z" - mac: ENC[AES256_GCM,data:UvfEnQuGSrXNe9Zyel2UEyfcAgh03UgBu0VzSg0lzOgYuK0qPwvezZNXRyfFkHo0AdpIJAmN8pwtavExUWqL8OOUgIcooK7hZJ9S5aXFfLeZ0YhpAZ1TpUmHTNRL7VMOJtLU/phvObggYTHJfLLE92RkwnKxqJ8hIli8VVIa59w=,iv:NYcAEyoNC8cygEczLGUXzuhY/OXTXCpP3BDQJJKQwQU=,tag:PgkGZwmkiCAkO/WPZJyDYQ==,type:str] + lastmodified: "2026-05-11T07:36:34Z" + mac: ENC[AES256_GCM,data:VDSB1iKcP9AY4HGHPlQxINlc2wB03EkCd8TcHLfFqE0zNBPYlX0Buf6kqy89/0M9UmRYGS7IlYMEDlFGJ61cidD0Kxk2n9R7P32S75nS//lorX/28Slnwlfe9K4w7eS5p+zwO0QiPY3X87or6KTyi95QBYz6ALp9cpetrRn3nJk=,iv:PPqdmFbXCFmZgfXsEUvuQy1PDde2dD/EnHAFE2TGZIk=,tag:Hv+QMEM8wkjXPUt4TGhvCQ==,type:str] unencrypted_suffix: _unencrypted - version: 3.13.2 + version: 3.12.2 diff --git a/secrets/tide.yaml.example b/secrets/tide.yaml.example index 8986906..40dfc8a 100644 --- a/secrets/tide.yaml.example +++ b/secrets/tide.yaml.example @@ -1,5 +1,20 @@ -# Encrypt this file with sops to produce ./tide.yaml. +# Encrypt this with sops to produce ./tide.yaml: +# +# sops --encrypt --age $(cat ~/.config/sops/age/keys.txt | grep public | cut -d: -f2 | tr -d ' ') \ +# secrets/tide.yaml.example > secrets/tide.yaml +# +# Or set up .sops.yaml with the host's age public key and run +# `sops secrets/tide.yaml`. +# +# Generate APP_KEY with: +# php -r "echo 'base64:'.base64_encode(random_bytes(32)).PHP_EOL;" -admin-password: REPLACE_ME -mail-key: REPLACE_ME -secret-key: REPLACE_ME +tide-env: | + APP_KEY=base64:REPLACE_ME + DB_PASSWORD=REPLACE_ME + MAIL_HOST=127.0.0.1 + MAIL_PORT=1025 + MAIL_USERNAME= + MAIL_PASSWORD= + MAIL_FROM_ADDRESS=noreply@tide.yisroelbaum.com + MAIL_FROM_NAME=TIDE diff --git a/tide.nix b/tide.nix index a34de74..80f5fdd 100644 --- a/tide.nix +++ b/tide.nix @@ -1,41 +1,37 @@ -{ config, ... }: +{ + domainName, + ... +}: { services.tide = { enable = true; - secretFiles = { - adminPassword = config.sops.secrets."tide-admin-password".path; - mailPassword = config.sops.secrets."tide-mail-password".path; - secretKeyBase = config.sops.secrets."tide-secret-key-base".path; - borgPassphrase = config.sops.secrets."borg-passphrase".path; - borgPrivateKey = config.sops.secrets."borg-private-key".path; - }; + domain = "tide.${domainName}"; + apiDomain = "apitide.${domainName}"; + secretsFile = "/run/secrets/tide-env"; + # Reuse the wildcard cert already issued for *.${domainName} + # in configuration.nix instead of requesting a new one per + # subdomain. + nginx.useACMEHost = null; }; + # Don't fail evaluation when secrets/tide.yaml is missing (e.g. + # before the operator has encrypted it on a fresh checkout). + # sops-install-secrets will still error at activation time if + # the file is absent, which is the right place for that failure. + sops.validateSopsFiles = false; + + # Use the host's age key generated with age-keygen, not the + # SSH-host-key-derived identity sops-nix falls back to by default. + # The encrypted file's recipient is the public key paired with + # this private key. sops.age.keyFile = "/var/lib/sops-nix/key.txt"; sops.age.sshKeyPaths = [ ]; sops.gnupg.sshKeyPaths = [ ]; - sops.secrets = { - "tide-admin-password" = { - sopsFile = ./secrets/tide.yaml; - key = "admin-password"; - mode = "0400"; - owner = "discourse"; - group = "discourse"; - }; - "tide-mail-password" = { - sopsFile = ./secrets/tide.yaml; - key = "mail-key"; - mode = "0400"; - owner = "discourse"; - group = "discourse"; - }; - "tide-secret-key-base" = { - sopsFile = ./secrets/tide.yaml; - key = "secret-key"; - mode = "0400"; - owner = "discourse"; - group = "discourse"; - }; + sops.secrets."tide-env" = { + sopsFile = ./secrets/tide.yaml; + # phpfpm reads this via EnvironmentFile, which runs as root + # before dropping to the tide user, so root readable is enough. + mode = "0400"; }; }