Adds services/dev/obsidian-livesync.nix — CouchDB 3 from the native
nixpkgs module, tuned as the backend for the Self-hosted LiveSync plugin
— and publishes it as notes.mgaction.town through neptun.
It goes out over the public reverse proxy rather than staying on the LAN
because Obsidian's mobile apps refuse cleartext HTTP and *.jupiter.sol
cannot hold a publicly trusted cert. That makes the hardening load-bearing
rather than decorative:
- require_valid_user in both [chttpd] and [chttpd_auth], so nothing
answers unauthenticated on the open internet;
- neptun's vhost matches on CouchDB's own naming rule (system endpoints
all begin with `_`, user databases never can), so Fauxton, /_all_dbs
and /_node/_local/_config — which rewrites the server config given
admin credentials — 404 at the proxy while any number of per-vault
databases pass. Verified against both sets of paths with caddy run
against a stub backend;
- the plugin's own E2EE carries the actual confidentiality: jupiter only
ever stores ciphertext. Its passphrase is deliberately NOT in sops —
it never leaves the clients, and pairing it with the server credential
would defeat the point.
flush_interval -1 is required, not tuning: replication rides a continuous
_changes feed that caddy would otherwise buffer into a stall.
Storage sits on the array with RequiresMountsFor, since a CouchDB that
starts without /mnt/data would create an empty database on the eMMC and
LiveSync would replicate that emptiness back to every client. Logs go to
journald rather than the unrotated /var/log/couchdb.log, for the same
29G-eMMC reasons as the rest of jupiter.
The admin password reaches CouchDB as an [admins] ini fragment via
extraConfigFiles; services.couchdb.adminPass would have rendered it into
the world-readable store.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLN5nkLBtCciD3ZnUwtw2b
90 lines
4.2 KiB
Nix
90 lines
4.2 KiB
Nix
{ config, ... }:
|
|
|
|
# sops-nix secret wiring (real host only; not imported by vm.nix).
|
|
# Encrypted values live in ../../secrets/jupiter.yaml, decrypted at activation to
|
|
# /run/secrets/<name>.
|
|
#
|
|
# The host decrypts with its OWN SSH host key (age identity derived via
|
|
# ssh-to-age, recipient listed in ../../.sops.yaml). The key is pre-generated on
|
|
# the laptop and shipped once at install as /etc/ssh/ssh_host_ed25519_key
|
|
# (nixos-anywhere --extra-files) — so decryption works on boot #1 and there is
|
|
# no separate sops-only key to manage.
|
|
{
|
|
sops.defaultSopsFile = ../../secrets/jupiter.yaml;
|
|
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
|
|
|
|
# Decrypts to /run/secrets/samba_password (root-only by default).
|
|
sops.secrets.samba_password = { };
|
|
|
|
# darman's login password (a sha-512 hash, not plaintext — generate with
|
|
# `mkpasswd -m sha-512`, edit via ./edit_secrets). neededForUsers makes it
|
|
# available before user setup, at /run/secrets-for-users/darman_password.
|
|
sops.secrets.darman_password.neededForUsers = true;
|
|
users.users.darman.hashedPasswordFile =
|
|
config.sops.secrets.darman_password.path;
|
|
|
|
# Headscale pre-auth key for tailscale auto-registration (see configuration.nix).
|
|
sops.secrets.tailscale_authkey = { };
|
|
|
|
# Immich's OIDC client secret, from its Authentik application (a SEPARATE
|
|
# app from headscale's and headplane's — see hosts/neptun/secrets.nix).
|
|
# Referenced as settings.oauth.clientSecret._secret in
|
|
# services/media/immich.nix; the module resolves it through systemd
|
|
# LoadCredential, which reads as root before dropping privileges, so the
|
|
# sops default of root:root 0400 is correct — do NOT set `owner`.
|
|
sops.secrets.immich_oauth_client_secret = { };
|
|
|
|
# Gitea Actions runner registration token (services/dev/gitea.nix). Gitea
|
|
# generates this itself once Actions is enabled — it is not a password
|
|
# chosen up front. Rendered into a `TOKEN=...` env file because
|
|
# gitea-actions-runner takes an EnvironmentFile, not a raw secret path.
|
|
sops.secrets.gitea_runner_token = { };
|
|
sops.templates."gitea-runner.env".content =
|
|
"TOKEN=${config.sops.placeholder.gitea_runner_token}";
|
|
|
|
# provisioning access token for gitea used to setup ci-bot account + repo access
|
|
sops.secrets.gitea_provisioning_token.owner = "gitea";
|
|
|
|
# ci-bot access token to allow the ci-bot user to push to repos
|
|
sops.secrets.gitea_ci_bot_token.owner = "gitea";
|
|
|
|
# Add the same value to secrets/jupiter.yaml before deploying Jupiter.
|
|
sops.secrets.gitea_hermes_webhook_secret = {
|
|
owner = "gitea";
|
|
};
|
|
|
|
# SABnzbd credentials (web UI login, API keys, eweka.nl usenet server) —
|
|
# migrated off the reused ini in services/media/sabnzbd.nix into
|
|
# services.sabnzbd.settings + secretValues. sabnzbd_api_key predates this
|
|
# migration (provisioned for mediamanager's future use, services/experimental/
|
|
# mediamanager.nix — not currently imported by any host); reused here as the
|
|
# same single source of truth rather than duplicating it.
|
|
# owner = sabnzbd: the module's preStart (replace-secret) runs as the
|
|
# service's own User=/Group=, and sops secrets default to root:root 0400 —
|
|
# without this, replace-secret gets Permission denied reading /run/secrets.
|
|
sops.secrets.sabnzbd_web_username.owner = "sabnzbd";
|
|
sops.secrets.sabnzbd_web_password.owner = "sabnzbd";
|
|
sops.secrets.sabnzbd_api_key.owner = "sabnzbd";
|
|
sops.secrets.sabnzbd_nzb_key.owner = "sabnzbd";
|
|
sops.secrets.sabnzbd_eweka_username.owner = "sabnzbd";
|
|
sops.secrets.sabnzbd_eweka_password.owner = "sabnzbd";
|
|
|
|
# CouchDB admin account for Obsidian LiveSync
|
|
# (services/dev/obsidian-livesync.nix). Rendered into an [admins] ini
|
|
# fragment rather than passed as services.couchdb.adminPass, which would put
|
|
# the plaintext in the world-readable store.
|
|
#
|
|
# owner = couchdb on BOTH: couchdb re-reads its ini chain as its own
|
|
# User=/Group= after systemd drops privileges, and sops defaults to
|
|
# root:root 0400 — without this it comes up with no admin configured, which
|
|
# under require_valid_user means every request 401s.
|
|
sops.secrets.couchdb_admin_password.owner = "couchdb";
|
|
sops.templates."couchdb-admins.ini" = {
|
|
owner = "couchdb";
|
|
content = ''
|
|
[admins]
|
|
obsidian = ${config.sops.placeholder.couchdb_admin_password}
|
|
'';
|
|
};
|
|
}
|