obsidian: self-hosted vault sync via CouchDB on jupiter
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
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
../../services/media/seerr.nix
|
||||
../../services/media/immich.nix
|
||||
../../services/dev/gitea.nix
|
||||
../../services/dev/obsidian-livesync.nix
|
||||
];
|
||||
|
||||
# sabnzbd's unrar dependency is unfree; scope the allowance to just that
|
||||
|
||||
@@ -69,4 +69,21 @@
|
||||
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}
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,6 +111,62 @@
|
||||
reverse_proxy http://jupiter.orbit.sol:2283
|
||||
'';
|
||||
|
||||
# ---- Obsidian LiveSync (CouchDB on jupiter) ----
|
||||
# Obsidian's mobile apps refuse cleartext HTTP and *.jupiter.sol cannot hold
|
||||
# a publicly trusted cert, so the vault database is published here instead of
|
||||
# staying on the LAN. That means a credentialed database on the open
|
||||
# internet; two things keep it sane:
|
||||
#
|
||||
# 1. The plugin's end-to-end encryption, switched on BEFORE the first sync.
|
||||
# jupiter then stores only ciphertext, so a breach here is not a leak of
|
||||
# the notes themselves.
|
||||
# 2. This allowlist. CouchDB serves far more than the replication API —
|
||||
# Fauxton (/_utils), /_all_dbs, and /_node/_local/_config, the last of
|
||||
# which REWRITES the server's config given admin credentials. Only the
|
||||
# paths the plugin actually speaks are proxied; everything else is
|
||||
# answered here and never reaches jupiter. Use the tailnet for the rest:
|
||||
# `curl http://jupiter.orbit.sol:5984/_utils/`.
|
||||
#
|
||||
# ONE DATABASE PER VAULT, and the matcher keys off CouchDB's own naming rule
|
||||
# rather than listing them: every system endpoint begins with `_`, and a
|
||||
# user-creatable database never can (CouchDB requires a lowercase letter
|
||||
# first). So adding a vault needs no edit here. `_session` is the single
|
||||
# underscore path let through, for cookie auth.
|
||||
#
|
||||
# The flip side of not listing them: a mistyped but otherwise LEGAL database
|
||||
# name is proxied through and reaches CouchDB, which answers a real 404 the
|
||||
# plugin can report. An ILLEGAL one — anything starting with a capital or an
|
||||
# underscore — fails the matcher instead and gets caddy's 404, which carries
|
||||
# no CORS headers and surfaces in Obsidian as a connection failure with no
|
||||
# error message at all. If a new vault refuses to connect and the plugin
|
||||
# says nothing, check the database name is lowercase first.
|
||||
#
|
||||
# Never point two vaults at one database: LiveSync merges them into a single
|
||||
# file tree, which is not cleanly reversible.
|
||||
#
|
||||
# Known consequence: LiveSync's "Check database configuration" panel reads
|
||||
# /_node/_local/_config and so reports the server as unconfigured from
|
||||
# outside. Expected — that config is declarative in
|
||||
# services/dev/obsidian-livesync.nix and is not the plugin's to patch.
|
||||
#
|
||||
# `flush_interval -1` is required, not tuning: replication rides a
|
||||
# continuous _changes feed, which caddy would otherwise buffer — sync then
|
||||
# stalls until the buffer fills (same reason vpn.mgaction.town sets it).
|
||||
#
|
||||
# No netcup edge-firewall change: this rides the 443 the other vhosts
|
||||
# already use, unlike gitea's :2222.
|
||||
services.caddy.virtualHosts."notes.mgaction.town".extraConfig = ''
|
||||
@livesync path_regexp ^/(_session|[a-z][a-z0-9_$()+-]*)?(/.*)?$
|
||||
handle @livesync {
|
||||
reverse_proxy http://jupiter.orbit.sol:5984 {
|
||||
flush_interval -1
|
||||
}
|
||||
}
|
||||
handle {
|
||||
respond 404
|
||||
}
|
||||
'';
|
||||
|
||||
# ---- Hermes dashboard ----
|
||||
# Authentik-gated (hosts/mars/hermes-agent.nix has the OIDC config and the
|
||||
# "create the Authentik app" instructions — moved here from jupiter).
|
||||
|
||||
Reference in New Issue
Block a user