Compare commits
3
Commits
15e081e52d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45e08e35a2 | ||
|
|
a8a1cffa3e | ||
|
|
15ae1cf608 |
@@ -427,6 +427,93 @@ another way in.
|
|||||||
(via the `/mnt/jupiter` samba mount) before the first switch if you want
|
(via the `/mnt/jupiter` samba mount) before the first switch if you want
|
||||||
it preserved instead of starting clean.
|
it preserved instead of starting clean.
|
||||||
|
|
||||||
|
### Obsidian vaults (jupiter CouchDB + mars bridge)
|
||||||
|
|
||||||
|
CouchDB itself is fully declarative (`services/dev/obsidian-livesync.nix`), but
|
||||||
|
three things are runtime state it cannot own.
|
||||||
|
|
||||||
|
**1. Each vault's database is created by the plugin.** Point Self-hosted
|
||||||
|
LiveSync at `https://notes.mgaction.town` (URI field) with the database name in
|
||||||
|
its own field — *not* as a path on the URI. Turn on End-to-End Encryption and
|
||||||
|
Obfuscate Properties **before the first sync**; both are remote-format
|
||||||
|
decisions and changing them later means converting or rebuilding the database.
|
||||||
|
The passphrase lives in the HomeLab Proton Pass vault, never in sops — it is
|
||||||
|
what keeps a publicly reachable database from being a readable one.
|
||||||
|
|
||||||
|
Database names must start with a lowercase letter (`a-z0-9_$()+-` after that).
|
||||||
|
An illegal name is rejected by neptun's matcher rather than CouchDB, and shows
|
||||||
|
up in Obsidian as a connection failure with **no error message at all**.
|
||||||
|
|
||||||
|
**2. luna's vault credentials on mars.** `hosts/mars/secrets.nix` needs two
|
||||||
|
values before mars will activate: `couchdb_luna_password` and
|
||||||
|
`obsidian_luna_passphrase`.
|
||||||
|
|
||||||
|
```
|
||||||
|
sops --set '["couchdb_luna_password"] "<password>"' secrets/mars.yaml
|
||||||
|
sops --set '["obsidian_luna_passphrase"] "<passphrase>"' secrets/mars.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep both alphanumeric. sops substitutes into already-rendered JSON, so a `"`
|
||||||
|
or `\` in either produces an invalid `config.json`; the bridge logs
|
||||||
|
`Could not parse configuration!` and then runs on with **zero peers** instead
|
||||||
|
of exiting, which looks exactly like a bridge that is simply idle.
|
||||||
|
|
||||||
|
As set up today these are the `obsidian` admin password and the same
|
||||||
|
passphrase as the personal vault, which means mars — the box running an
|
||||||
|
autonomous agent — can decrypt and read every vault database. Optional
|
||||||
|
hardening, either half independently:
|
||||||
|
|
||||||
|
```
|
||||||
|
# password comes straight out of sops; never echo it
|
||||||
|
LUNA_PW=$(sops --decrypt --extract '["couchdb_luna_password"]' secrets/mars.yaml)
|
||||||
|
ADMIN=obsidian # prompts for the admin password
|
||||||
|
curl -u "$ADMIN" -X PUT http://jupiter.orbit.sol:5984/_users/org.couchdb.user:luna \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "{\"name\":\"luna\",\"type\":\"user\",\"roles\":[],\"password\":\"$LUNA_PW\"}"
|
||||||
|
curl -u "$ADMIN" -X PUT http://jupiter.orbit.sol:5984/luna_wiki/_security \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"admins":{"names":[],"roles":[]},"members":{"names":["luna"],"roles":[]}}'
|
||||||
|
unset LUNA_PW
|
||||||
|
```
|
||||||
|
|
||||||
|
then set `username` in `hosts/mars/livesync-bridge.nix` to `luna` and put that
|
||||||
|
account's password in `couchdb_luna_password`. Run it against jupiter over the
|
||||||
|
tailnet — `/_users` is blocked on the public vhost on purpose. A vault-specific
|
||||||
|
passphrase is the other half, changed in the plugin and mirrored into sops.
|
||||||
|
|
||||||
|
**3. The database name must match.** `database` in
|
||||||
|
`hosts/mars/livesync-bridge.nix` has to be exactly the name entered in the
|
||||||
|
plugin. A mismatch does not error — with an admin credential PouchDB simply
|
||||||
|
creates the misnamed database and replicates an empty vault into it.
|
||||||
|
|
||||||
|
Order matters: set the vault up from Obsidian first so the database exists and
|
||||||
|
carries the plugin's own tweaks, then deploy mars. Afterwards:
|
||||||
|
|
||||||
|
```
|
||||||
|
systemctl status livesync-bridge # on mars
|
||||||
|
cat /var/lib/livesync-bridge/health.json # per-peer ok/backendUp/detail
|
||||||
|
ls /var/lib/livesync-bridge/vault # her notes, as real markdown
|
||||||
|
```
|
||||||
|
|
||||||
|
The vault is mounted into the agent container at `/opt/data/vault`, inside
|
||||||
|
`HERMES_WRITE_SAFE_ROOT`, so luna can write as well as read.
|
||||||
|
|
||||||
|
A note luna writes reaches CouchDB as soon as the bridge sees it, but whether
|
||||||
|
it then reaches your devices depends on that vault's **Sync Mode** in the
|
||||||
|
plugin. Only "LiveSync (real-time)" pulls continuously; the periodic/on-save
|
||||||
|
presets need their timer or a manual **Replicate**. A file that appears only
|
||||||
|
after clicking Replicate is the client waiting, not the bridge failing — the
|
||||||
|
database already had it. Check the bridge's own side in the journal:
|
||||||
|
|
||||||
|
```
|
||||||
|
journalctl -u livesync-bridge | grep -- '--> luna-remote'
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Verify her writes actually land before trusting this.** Upstream has three
|
||||||
|
open issues on the storage→CouchDB direction (#50, #23, #46) and all fail
|
||||||
|
silently — the log reports the upload and the database never updates. Create a
|
||||||
|
note as luna, confirm it appears on a phone, and re-check after any input bump.
|
||||||
|
|
||||||
### mercury (Raspberry Pi 3B+)
|
### mercury (Raspberry Pi 3B+)
|
||||||
|
|
||||||
- `./deploy flash mercury /dev/sdX` writes the dedicated age key to the root
|
- `./deploy flash mercury /dev/sdX` writes the dedicated age key to the root
|
||||||
|
|||||||
Generated
+17
@@ -176,6 +176,22 @@
|
|||||||
"url": "https://git.mgaction.town/darman/hypr-chrome.git"
|
"url": "https://git.mgaction.town/darman/hypr-chrome.git"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"livesync-bridge": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1787571662,
|
||||||
|
"narHash": "sha256-btLnQNbFzCPaSVcY9rtiPdYeXrZjoK9AYvfA9+ovsIc=",
|
||||||
|
"owner": "vrtmrz",
|
||||||
|
"repo": "livesync-bridge",
|
||||||
|
"rev": "c3760beaa0851214da4860903445d7f6420ca025",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "vrtmrz",
|
||||||
|
"repo": "livesync-bridge",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"media-manager": {
|
"media-manager": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
@@ -490,6 +506,7 @@
|
|||||||
"disko": "disko",
|
"disko": "disko",
|
||||||
"home-manager": "home-manager",
|
"home-manager": "home-manager",
|
||||||
"hypr-chrome": "hypr-chrome",
|
"hypr-chrome": "hypr-chrome",
|
||||||
|
"livesync-bridge": "livesync-bridge",
|
||||||
"mediamanager-nix": "mediamanager-nix",
|
"mediamanager-nix": "mediamanager-nix",
|
||||||
"nix-flatpak": "nix-flatpak",
|
"nix-flatpak": "nix-flatpak",
|
||||||
"nixos-anywhere": "nixos-anywhere",
|
"nixos-anywhere": "nixos-anywhere",
|
||||||
|
|||||||
@@ -31,6 +31,18 @@
|
|||||||
url = "github:strangeglyph/mediamanager-nix";
|
url = "github:strangeglyph/mediamanager-nix";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
# livesync-bridge — headless CouchDB <-> filesystem sync for Obsidian
|
||||||
|
# LiveSync, used on mars to give luna a real directory of markdown
|
||||||
|
# (hosts/mars/livesync-bridge.nix). Not a flake and not in nixpkgs, so it
|
||||||
|
# comes in as plain source pinned by flake.lock; the service copies it out
|
||||||
|
# and runs it under deno. Pinning matters more than usual here — this is a
|
||||||
|
# small third-party project with open bugs on the storage->couchdb path,
|
||||||
|
# so an unreviewed bump could quietly change how the agent's notes are
|
||||||
|
# written back.
|
||||||
|
livesync-bridge = {
|
||||||
|
url = "github:vrtmrz/livesync-bridge";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
authentik-nix.url = "github:nix-community/authentik-nix";
|
authentik-nix.url = "github:nix-community/authentik-nix";
|
||||||
nix-flatpak.url = "github:gmodena/nix-flatpak";
|
nix-flatpak.url = "github:gmodena/nix-flatpak";
|
||||||
# Own Hyprland plugin (border + title bar), public repo, fetched over
|
# Own Hyprland plugin (border + title bar), public repo, fetched over
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
../../services/media/seerr.nix
|
../../services/media/seerr.nix
|
||||||
../../services/media/immich.nix
|
../../services/media/immich.nix
|
||||||
../../services/dev/gitea.nix
|
../../services/dev/gitea.nix
|
||||||
|
../../services/dev/obsidian-livesync.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
# sabnzbd's unrar dependency is unfree; scope the allowance to just that
|
# 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_username.owner = "sabnzbd";
|
||||||
sops.secrets.sabnzbd_eweka_password.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}
|
||||||
|
'';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
./disk-config.nix # disko: OS-disk partitions + filesystems
|
./disk-config.nix # disko: OS-disk partitions + filesystems
|
||||||
./secrets.nix # sops-nix: samba/tailscale/hermes secrets
|
./secrets.nix # sops-nix: samba/tailscale/hermes secrets
|
||||||
./hermes-agent.nix
|
./hermes-agent.nix
|
||||||
|
./livesync-bridge.nix
|
||||||
../../common.nix # shared base: user / ssh / nix / firewall
|
../../common.nix # shared base: user / ssh / nix / firewall
|
||||||
../../services/containers.nix
|
../../services/containers.nix
|
||||||
../../services/vpn/tailscale.nix
|
../../services/vpn/tailscale.nix
|
||||||
|
|||||||
@@ -278,6 +278,13 @@ in
|
|||||||
"${hermesHome}:/opt/data"
|
"${hermesHome}:/opt/data"
|
||||||
"${dropboxDir}:/opt/data/dropbox"
|
"${dropboxDir}:/opt/data/dropbox"
|
||||||
|
|
||||||
|
# luna's Obsidian vault, kept in sync with CouchDB on jupiter by
|
||||||
|
# livesync-bridge.nix. Under /opt/data so it lands inside
|
||||||
|
# HERMES_WRITE_SAFE_ROOT and she can write notes, not just read them —
|
||||||
|
# same reasoning as the dropbox above. The bridge runs as this very
|
||||||
|
# uid/gid, so no ownership fixup is needed on either side.
|
||||||
|
"/var/lib/livesync-bridge/vault:/opt/data/vault"
|
||||||
|
|
||||||
# git/tea for luna: the image doesn't ship `tea` (and shouldn't be
|
# git/tea for luna: the image doesn't ship `tea` (and shouldn't be
|
||||||
# trusted to have a known-good `git` either), so both come from this
|
# trusted to have a known-good `git` either), so both come from this
|
||||||
# host's Nix store instead — mounted read-only at fixed PATH-visible
|
# host's Nix store instead — mounted read-only at fixed PATH-visible
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
{ config, pkgs, inputs, ... }:
|
||||||
|
|
||||||
|
# livesync-bridge (vrtmrz) — mirrors an Obsidian LiveSync vault out of CouchDB
|
||||||
|
# on jupiter (services/dev/obsidian-livesync.nix) into a real directory of
|
||||||
|
# markdown here, so luna can read and write the vault as files. Obsidian itself
|
||||||
|
# is an Electron GUI with no headless mode, and an agent wants files anyway.
|
||||||
|
#
|
||||||
|
# ⚠️ THE WRITE-BACK PATH IS THE RISKY ONE. Upstream has three open, unanswered
|
||||||
|
# issues on storage->couchdb — #50 (Jun 2026, writes detected and logged as
|
||||||
|
# uploaded, database never updated), #23 (only lowercase filenames transmitted
|
||||||
|
# from storage), #46 (silent stall on files over ~30KB). All fail QUIETLY: the
|
||||||
|
# log says success and the note never arrives. So do not treat this directory
|
||||||
|
# as durable storage for anything luna cannot regenerate, and check that her
|
||||||
|
# edits actually reach your devices before trusting it. (E2EE itself is fine —
|
||||||
|
# PeerCouchDB.ts hard-errors if a passphrase is missing for an encrypted
|
||||||
|
# remote, so it is a deliberate code path. The one issue claiming E2EE breaks
|
||||||
|
# bridging, #12, is a single unreproduced report with no maintainer reply.)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# EXPECTED NOISE ON FIRST SYNC: a stack trace per historically-deleted file —
|
||||||
|
# NotFound: ... remove '<vault>/Welcome.md' at PeerStorage.delete
|
||||||
|
# CouchDB keeps deletion tombstones, and the bridge replays them against a
|
||||||
|
# directory where the file never existed. PeerStorage.ts:33-40 catches it,
|
||||||
|
# logs, and returns false, so nothing is wrong; it only LOOKS fatal because
|
||||||
|
# main.ts pins the logger to LOG_LEVEL_DEBUG, which prints exception dumps
|
||||||
|
# that are otherwise verbose-level. It stops once the initial catch-up ends.
|
||||||
|
# Talks to CouchDB over the TAILNET (jupiter.orbit.sol:5984), not through
|
||||||
|
# neptun: mars is a tailnet node, so the public vhost, its TLS and its path
|
||||||
|
# allowlist are all irrelevant here.
|
||||||
|
let
|
||||||
|
stateDir = "/var/lib/livesync-bridge";
|
||||||
|
appDir = "${stateDir}/app";
|
||||||
|
vaultDir = "${stateDir}/vault";
|
||||||
|
|
||||||
|
# The same uid/gid the hermes-agent container runs as (hermes-agent.nix).
|
||||||
|
# Deliberate: the bridge and luna both read and write these files, and
|
||||||
|
# sharing one uid removes any dependence on the container's umask. Two
|
||||||
|
# different uids in a shared group only works while every file stays
|
||||||
|
# group-writable, and a single 0644 file dropped by the agent would stall
|
||||||
|
# sync on that path with nothing but a permission error in the log.
|
||||||
|
hermesUid = 986;
|
||||||
|
|
||||||
|
# Which vault. `group` is what pairs the two peers — both must match or the
|
||||||
|
# bridge starts cleanly and simply never syncs anything.
|
||||||
|
#
|
||||||
|
# ⚠️ `database` must be the name entered in the Obsidian plugin for luna's
|
||||||
|
# vault. Get it wrong and nothing errors: the credential below is CouchDB's
|
||||||
|
# admin, so PouchDB CREATES the misnamed database and replicates an empty
|
||||||
|
# vault into it quite happily.
|
||||||
|
peerGroup = "luna";
|
||||||
|
database = "luna_wiki";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# hermes-agent.nix declares the GROUP (gid 983) but no user: the container
|
||||||
|
# brings its own uid and needs no host account. The bridge does need one to
|
||||||
|
# run as, so the matching user is declared here.
|
||||||
|
users.users.hermes = {
|
||||||
|
uid = hermesUid;
|
||||||
|
group = "hermes";
|
||||||
|
isSystemUser = true;
|
||||||
|
home = stateDir;
|
||||||
|
description = "Hermes agent uid, shared with the livesync-bridge service";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Created here rather than by the service so they exist before anything
|
||||||
|
# tries to use them:
|
||||||
|
# - vaultDir before podman-hermes-agent starts, because a bind-mount
|
||||||
|
# source that does not exist is created by podman as root:root and the
|
||||||
|
# bridge then cannot write into its own vault;
|
||||||
|
# - appDir because WorkingDirectory applies to ExecStartPre as well, so a
|
||||||
|
# missing one fails the unit before preStart ever gets to create it.
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${vaultDir} 0770 hermes hermes -"
|
||||||
|
"d ${appDir} 0750 hermes hermes -"
|
||||||
|
"d ${stateDir}/deno 0750 hermes hermes -"
|
||||||
|
];
|
||||||
|
|
||||||
|
# The bridge's peer config, rendered by sops because it carries three
|
||||||
|
# secrets inline (CouchDB password + both passphrases) and the file format
|
||||||
|
# has no include mechanism.
|
||||||
|
#
|
||||||
|
# ⚠️ sops substitutes placeholders into the ALREADY-RENDERED json, so a
|
||||||
|
# secret containing a double quote or a backslash produces an invalid config
|
||||||
|
# and the bridge logs "Could not parse configuration!" and then sits there
|
||||||
|
# with zero peers — it does not exit. Keep all three values alphanumeric.
|
||||||
|
sops.templates."livesync-bridge.json" = {
|
||||||
|
owner = "hermes";
|
||||||
|
content = builtins.toJSON {
|
||||||
|
peers = [
|
||||||
|
{
|
||||||
|
type = "couchdb";
|
||||||
|
name = "luna-remote";
|
||||||
|
group = peerGroup;
|
||||||
|
url = "http://jupiter.orbit.sol:5984";
|
||||||
|
inherit database;
|
||||||
|
username = "obsidian";
|
||||||
|
password = config.sops.placeholder.couchdb_luna_password;
|
||||||
|
passphrase = config.sops.placeholder.obsidian_luna_passphrase;
|
||||||
|
# The plugin derives path obfuscation from the same passphrase it
|
||||||
|
# uses for content, so this is the same secret. Split into its own
|
||||||
|
# field because the bridge takes them separately — if paths come
|
||||||
|
# back as garbage while contents decode fine, this is the field that
|
||||||
|
# is wrong.
|
||||||
|
obfuscatePassphrase = config.sops.placeholder.obsidian_luna_passphrase;
|
||||||
|
# Reads the chunking tweaks the plugin stored in the remote, instead
|
||||||
|
# of guessing sizes that then disagree with every other client.
|
||||||
|
useRemoteTweaks = true;
|
||||||
|
baseDir = "";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type = "storage";
|
||||||
|
name = "luna-vault";
|
||||||
|
group = peerGroup;
|
||||||
|
baseDir = vaultDir;
|
||||||
|
# Catch up on anything that changed while the service was down.
|
||||||
|
scanOfflineChanges = true;
|
||||||
|
useChokidar = true;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.livesync-bridge = {
|
||||||
|
description = "Obsidian LiveSync bridge (CouchDB <-> ${vaultDir})";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network-online.target" "tailscaled.service" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
# Persistent module + npm cache. Without a fixed DENO_DIR the service
|
||||||
|
# re-downloads its whole dependency tree on every start.
|
||||||
|
DENO_DIR = "${stateDir}/deno";
|
||||||
|
# main.ts reads this instead of ./dat/config.json, which keeps the
|
||||||
|
# secret out of the copied source tree entirely.
|
||||||
|
LSB_CONFIG = config.sops.templates."livesync-bridge.json".path;
|
||||||
|
LSB_HEALTH_FILE = "${stateDir}/health.json";
|
||||||
|
HOME = stateDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Copy the pinned source out of the store and install its locked deps.
|
||||||
|
# It cannot run from /nix/store directly: deno.jsonc sets
|
||||||
|
# `nodeModulesDir: manual` with byonm, so `deno install` must write a
|
||||||
|
# node_modules/ next to the sources.
|
||||||
|
#
|
||||||
|
# The copy target is a FIXED path on purpose. Deno keys localStorage —
|
||||||
|
# which is where the bridge records per-file sync state (Peer.ts:119) — by
|
||||||
|
# the main module's origin, and stores it under
|
||||||
|
# DENO_DIR/location_data/<sha of that origin>. VERIFIED by running the same
|
||||||
|
# source from two paths against one DENO_DIR: two separate origin dirs
|
||||||
|
# appear. Running straight from /nix/store would therefore change the
|
||||||
|
# origin on every input bump and silently reset the bridge to a full
|
||||||
|
# rescan of both peers.
|
||||||
|
#
|
||||||
|
# Guarded by a stamp file so this is a no-op on ordinary restarts; only a
|
||||||
|
# flake input bump pays for the re-install (which needs network).
|
||||||
|
preStart = ''
|
||||||
|
set -eu
|
||||||
|
stamp=${stateDir}/.src
|
||||||
|
if [ "$(cat "$stamp" 2>/dev/null || true)" != "${inputs.livesync-bridge}" ]; then
|
||||||
|
# Contents only — appDir is this unit's WorkingDirectory, and
|
||||||
|
# deleting the cwd out from under deno breaks the install below.
|
||||||
|
find ${appDir} -mindepth 1 -delete
|
||||||
|
cp -r ${inputs.livesync-bridge}/. ${appDir}/
|
||||||
|
chmod -R u+w ${appDir}
|
||||||
|
${pkgs.deno}/bin/deno install --frozen
|
||||||
|
printf '%s' "${inputs.livesync-bridge}" > "$stamp"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
User = "hermes";
|
||||||
|
Group = "hermes";
|
||||||
|
StateDirectory = "livesync-bridge";
|
||||||
|
WorkingDirectory = appDir;
|
||||||
|
# `deno task run` is `deno run -A main.ts`; invoked directly so the
|
||||||
|
# task runner is not in the supervision path.
|
||||||
|
ExecStart = "${pkgs.deno}/bin/deno run -A main.ts";
|
||||||
|
# main.ts installs an unhandledrejection guard, but a genuinely dead
|
||||||
|
# process should still come back rather than trip the start limit.
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = 30;
|
||||||
|
# Group-writable output, so the two identities stay interchangeable if
|
||||||
|
# the uid sharing above is ever unpicked.
|
||||||
|
UMask = "0007";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -65,4 +65,36 @@
|
|||||||
# so they're visible inside the container at /opt/data/....
|
# so they're visible inside the container at /opt/data/....
|
||||||
# restartUnits re-provisions both on rotation, without a full mars deploy.
|
# restartUnits re-provisions both on rotation, without a full mars deploy.
|
||||||
sops.secrets.gitea_luna_token.restartUnits = [ "hermes-agent-prepare-dirs.service" ];
|
sops.secrets.gitea_luna_token.restartUnits = [ "hermes-agent-prepare-dirs.service" ];
|
||||||
|
|
||||||
|
# livesync-bridge (livesync-bridge.nix) — luna's Obsidian vault, mirrored
|
||||||
|
# out of CouchDB on jupiter. Both values are consumed by the rendered
|
||||||
|
# config.json rather than read directly, so the sops default of root:root
|
||||||
|
# 0400 is correct here; only the TEMPLATE needs an owner (set where it is
|
||||||
|
# defined, next to the vault path it references).
|
||||||
|
#
|
||||||
|
# couchdb_luna_password holds jupiter's `obsidian` ADMIN password — the same
|
||||||
|
# value as secrets/jupiter.yaml's couchdb_admin_password — and
|
||||||
|
# obsidian_luna_passphrase is the same passphrase as the personal vault.
|
||||||
|
# That is a deliberate choice to reuse what already existed, but it is worth
|
||||||
|
# being clear about what it costs: mars can decrypt and read EVERY vault
|
||||||
|
# database, not just luna's, and mars is the box running an autonomous
|
||||||
|
# agent. The two are independent to fix, cheapest first:
|
||||||
|
#
|
||||||
|
# 1. A vault-specific passphrase (re-encrypts luna's remote database, but
|
||||||
|
# leaves the personal vault's contents unreadable from here).
|
||||||
|
# 2. A CouchDB account scoped to luna's database via _security (three curl
|
||||||
|
# calls, in README -> "Obsidian vaults"), which also stops mars from
|
||||||
|
# reaching the other databases at all.
|
||||||
|
#
|
||||||
|
# Neither is required for the bridge to work; both shrink the blast radius
|
||||||
|
# if mars is ever compromised.
|
||||||
|
sops.secrets.couchdb_luna_password = { };
|
||||||
|
|
||||||
|
# The E2EE passphrase for luna's vault, as entered in the Obsidian plugin.
|
||||||
|
# Vault passphrases otherwise never leave the clients (see the note in
|
||||||
|
# services/dev/obsidian-livesync.nix) — this one has to be here because mars
|
||||||
|
# IS a client: it decrypts in order to write real markdown to disk. Path
|
||||||
|
# obfuscation uses the same passphrase in the plugin, so the bridge's
|
||||||
|
# separate obfuscatePassphrase field is fed from this one value.
|
||||||
|
sops.secrets.obsidian_luna_passphrase = { };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,62 @@
|
|||||||
reverse_proxy http://jupiter.orbit.sol:2283
|
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 ----
|
# ---- Hermes dashboard ----
|
||||||
# Authentik-gated (hosts/mars/hermes-agent.nix has the OIDC config and the
|
# Authentik-gated (hosts/mars/hermes-agent.nix has the OIDC config and the
|
||||||
# "create the Authentik app" instructions — moved here from jupiter).
|
# "create the Authentik app" instructions — moved here from jupiter).
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ sabnzbd_nzb_key: ENC[AES256_GCM,data:DNVenqhJ7wf5Ng0XRA1gJN95e+90e6D9NImOSHJv/Us
|
|||||||
sabnzbd_eweka_username: ENC[AES256_GCM,data:eLsTZoM8T8fAlGaXWlDaoQ==,iv:eawyGhN7+d6UfBIbI3y1qgq+MYBGrXP6VfAkSOK6llA=,tag:ELOfQGHU5NOxZFhKOKf8LA==,type:str]
|
sabnzbd_eweka_username: ENC[AES256_GCM,data:eLsTZoM8T8fAlGaXWlDaoQ==,iv:eawyGhN7+d6UfBIbI3y1qgq+MYBGrXP6VfAkSOK6llA=,tag:ELOfQGHU5NOxZFhKOKf8LA==,type:str]
|
||||||
sabnzbd_eweka_password: ENC[AES256_GCM,data:Mt3ZHAe2wzacCQq3x9Uy8WxjrVNad1SmU6sl8ZgrkMLymfq2eP4JzO/uPdD33A==,iv:PnFT95Zxqz4QBpPF5PRloKpoa15AU7Ef/Owwy+iDotw=,tag:/uRX00RzHLJN3gws5Qz8SA==,type:str]
|
sabnzbd_eweka_password: ENC[AES256_GCM,data:Mt3ZHAe2wzacCQq3x9Uy8WxjrVNad1SmU6sl8ZgrkMLymfq2eP4JzO/uPdD33A==,iv:PnFT95Zxqz4QBpPF5PRloKpoa15AU7Ef/Owwy+iDotw=,tag:/uRX00RzHLJN3gws5Qz8SA==,type:str]
|
||||||
gitea_hermes_webhook_secret: ENC[AES256_GCM,data:Q8e+mj05MJI7CEJwRonpOmQphAZ0CfnZFoGxrDSSiyHoH3BNhqBU5gBmzuu+6NK9OS33kN+JnFvrwCeEzVxooA==,iv:mdsKOMD5B0Jzh1YRmRh71P8Io9RFtI6aqAky5x+WxOQ=,tag:v3LKz5a8ayl7WAzIPbwj6Q==,type:str]
|
gitea_hermes_webhook_secret: ENC[AES256_GCM,data:Q8e+mj05MJI7CEJwRonpOmQphAZ0CfnZFoGxrDSSiyHoH3BNhqBU5gBmzuu+6NK9OS33kN+JnFvrwCeEzVxooA==,iv:mdsKOMD5B0Jzh1YRmRh71P8Io9RFtI6aqAky5x+WxOQ=,tag:v3LKz5a8ayl7WAzIPbwj6Q==,type:str]
|
||||||
|
couchdb_admin_password: ENC[AES256_GCM,data:QHkCFUwLbQdd5yKETI5qAz4CkEfsPcl2iCU8F9mX3PA=,iv:2dPNKjjoXEgm7wfC6MlhQTMvAXSNDtaXnjWl2ldl4fk=,tag:1ZltqH94Q5/6GbXAnaIgdg==,type:str]
|
||||||
sops:
|
sops:
|
||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
@@ -35,7 +36,7 @@ sops:
|
|||||||
CzjSDQZTcseEXZNwuzZcfB5Mvq0BQvjOj7lGuxzuE4qwWkdJWGfVLQ==
|
CzjSDQZTcseEXZNwuzZcfB5Mvq0BQvjOj7lGuxzuE4qwWkdJWGfVLQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt
|
recipient: age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt
|
||||||
lastmodified: "2026-08-23T03:16:52Z"
|
lastmodified: "2026-08-25T20:39:37Z"
|
||||||
mac: ENC[AES256_GCM,data:uQcOxORIWugK43LpQLI7JEjH6oGooseKCQQt0d+n43i7o23JGdUN5Wy/iD7GqmtVVZod02gl1ohEXV+kpvgetFpAO5NZu76HUVPFgaLOx+2LjrR1pNpC+52Iqlx52uypwby9eDvnC01jLFHu2l13NGBrLM3JQGmEXF57phzM/Q4=,iv:H7o3gdx/1GmZ1FRm7z97TNmiVpm6YFCEk0Puw4ZETDs=,tag:bsz3bcK2z3szrwpo55bSzQ==,type:str]
|
mac: ENC[AES256_GCM,data:Z59BCw8gETfddXqul4LXrq6V3LBJA1itF7A1VNUERwK4NfaUGwWUhbl9h7YF/srzgtg9yGjbFB/f5kwmT3k/TWTG+C0M/4KOyTVs4y5UvB9gI4g8awYbtnFDPRAcqqcxoMD0sgapgVcNh48KWv76ndF6UGn+QfWn9eF4KBP+ZzQ=,iv:57myu1aTMMSLTz+1ldwxdusnzh8cyPwrLiEIx3rLS9w=,tag:isthpdOydD4ZNoFZyflViw==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.13.3
|
version: 3.13.3
|
||||||
|
|||||||
+4
-2
@@ -6,6 +6,8 @@ telegram_bot_token: ENC[AES256_GCM,data:WX+KFtoqFodkoWNwd7EXUrUJakZ9oaMZgg4OnCeL
|
|||||||
hermes_dashboard_oidc_client_secret: ENC[AES256_GCM,data:IMPNTPMKO+b7eyV4hyGfnvH1/i+W4IPDNjncoyB1oIV8WaB6nOJn0sSEuTUCKB94K+Y7bsVQU0zpbKdIYOdGqgmPzwMCsScxMt4SewTmiiqWxv6SQFf4EzMxgXqjMvH8PWDzLcI2C2tI/KcVS251iqRViOTFe1/tkm+mV8sJmEI=,iv:F/rOUDmJZoGPS9fObAni5ntyOqbbhMWDPdHGLTexwlA=,tag:ALf98DmB0JziGspZMiLCiw==,type:str]
|
hermes_dashboard_oidc_client_secret: ENC[AES256_GCM,data:IMPNTPMKO+b7eyV4hyGfnvH1/i+W4IPDNjncoyB1oIV8WaB6nOJn0sSEuTUCKB94K+Y7bsVQU0zpbKdIYOdGqgmPzwMCsScxMt4SewTmiiqWxv6SQFf4EzMxgXqjMvH8PWDzLcI2C2tI/KcVS251iqRViOTFe1/tkm+mV8sJmEI=,iv:F/rOUDmJZoGPS9fObAni5ntyOqbbhMWDPdHGLTexwlA=,tag:ALf98DmB0JziGspZMiLCiw==,type:str]
|
||||||
gitea_luna_token: ENC[AES256_GCM,data:0ypW9oVFs1mXYPhPareMFRdkSYcvHSCm+fQOd7/76lJEXi217r9dmg==,iv:j3TPm/iLk6pB6CmDePFBOlnhxWSbmLKvOhz06SM1T7k=,tag:ydErvC2mZ1RRnwNffiHkkg==,type:str]
|
gitea_luna_token: ENC[AES256_GCM,data:0ypW9oVFs1mXYPhPareMFRdkSYcvHSCm+fQOd7/76lJEXi217r9dmg==,iv:j3TPm/iLk6pB6CmDePFBOlnhxWSbmLKvOhz06SM1T7k=,tag:ydErvC2mZ1RRnwNffiHkkg==,type:str]
|
||||||
gitea_hermes_webhook_secret: ENC[AES256_GCM,data:lV78H0xAehPxusSO/QruOYkt7fkMJrW+ScZL4UWYvgnBGn/D+1XHYPyHCqe2sEEWSlIaAgWMMoZzoVJ1Z1NFVQ==,iv:GmTZxoH2iiL/vTVgPfziXIFYD+Rl3cbh9hqXvWps+iw=,tag:jtXEUOVFfKrpTRK7S9PZMA==,type:str]
|
gitea_hermes_webhook_secret: ENC[AES256_GCM,data:lV78H0xAehPxusSO/QruOYkt7fkMJrW+ScZL4UWYvgnBGn/D+1XHYPyHCqe2sEEWSlIaAgWMMoZzoVJ1Z1NFVQ==,iv:GmTZxoH2iiL/vTVgPfziXIFYD+Rl3cbh9hqXvWps+iw=,tag:jtXEUOVFfKrpTRK7S9PZMA==,type:str]
|
||||||
|
couchdb_luna_password: ENC[AES256_GCM,data:V91is2h7UskI1rtwMzQyduNXoDPTYNwa4sw9K9WU+wE=,iv:k976ImKR19+CvGOVsHsrqMaFFtSQxVi6zABCJuQ5AWE=,tag:W2SN4SET/+o4+Wt6BOJ1LA==,type:str]
|
||||||
|
obsidian_luna_passphrase: ENC[AES256_GCM,data:fqHtP3g4J40ddYL9lzCixrisdC/DEJJermE=,iv:FU6BGNcBnyP8Rz3dNBk0+K0aAQTDW6/0aVaFm1rBFkk=,tag:Mg1aKqh++2rmU8ROaVIgDw==,type:str]
|
||||||
sops:
|
sops:
|
||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
@@ -26,7 +28,7 @@ sops:
|
|||||||
oyJ7PS3lW+PxH5AZkeeU7gXO/pz2oDku0aDOds7kaD3n0+qSWicQ+Q==
|
oyJ7PS3lW+PxH5AZkeeU7gXO/pz2oDku0aDOds7kaD3n0+qSWicQ+Q==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1eapjg6tdrr0fuvmgs3q3nlvnjkaxez298qynqqqxt0lpcv0lrsyq7ayxjk
|
recipient: age1eapjg6tdrr0fuvmgs3q3nlvnjkaxez298qynqqqxt0lpcv0lrsyq7ayxjk
|
||||||
lastmodified: "2026-08-23T05:55:49Z"
|
lastmodified: "2026-08-25T21:51:24Z"
|
||||||
mac: ENC[AES256_GCM,data:a3vCmrQMCS25tNWrzTeiGmOHf4Fn356PO3uNa2HvS21EBCKTc6YWBj9KmpORdz+6t03JJe/4eiGdghGaLhRr+JXyQnaT54gSV+FhC3dH6blind746XN3h+Z9rxiva6apvcAGUZ9k01Js5IXN9efEMhcI6w0U4oVuVqtvShvg8A8=,iv:9kF3cJ1vyy2H3eH10DVCYmWeXv2MH4AFDiF8cOajlw4=,tag:zhombLVpL8M1TUtYur/gYQ==,type:str]
|
mac: ENC[AES256_GCM,data:dz249hf3w8Tn0JStFOhhpdCZFMx2yxmNABx1CbeIQ/JlICAU82e4fg8AzJQY9EMOEs3Zx6L61yieljD4A/HLip5rDVlAXqqLeklW60eb7BHuSO79YfFgot+rS05g8WqFkRJYWzmkXhMbErCI133n72XEdeqMUU/m0djOUZlVRLs=,iv:d7G6TJRLfmXvQ2BUG9Hi83lOB9anhmb9qneLeVSCBhc=,tag:F6X/0z00PjiYum5kf8YApA==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.13.3
|
version: 3.13.3
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
{ config, ... }:
|
||||||
|
|
||||||
|
# CouchDB, tuned as the backend for Obsidian Self-hosted LiveSync
|
||||||
|
# (vrtmrz/obsidian-livesync). The plugin replicates the vault into CouchDB
|
||||||
|
# chunk-by-chunk over PouchDB's replication protocol, so this is a plain
|
||||||
|
# CouchDB 3 node — nothing Obsidian-specific runs here.
|
||||||
|
#
|
||||||
|
# Published PUBLICLY as https://notes.mgaction.town via neptun's caddy (see
|
||||||
|
# hosts/neptun/configuration.nix), because Obsidian's mobile apps refuse
|
||||||
|
# cleartext HTTP and jupiter's *.jupiter.sol names cannot get a real cert.
|
||||||
|
# That makes the settings below security-relevant, not cosmetic:
|
||||||
|
#
|
||||||
|
# - `require_valid_user` in BOTH [chttpd] and [chttpd_auth]: without it
|
||||||
|
# CouchDB answers unauthenticated GETs on the open internet.
|
||||||
|
# - neptun's vhost allowlists only the endpoints the plugin uses, so Fauxton
|
||||||
|
# (/_utils) and the cluster/config endpoints are not reachable from
|
||||||
|
# outside at all — reach them over the tailnet instead.
|
||||||
|
# - Turn ON end-to-end encryption in the plugin (Settings → Remote Database
|
||||||
|
# → End-to-End Encryption, plus "Obfuscate Properties", which covers the
|
||||||
|
# paths and timestamps that E2EE alone leaves readable). Then this server
|
||||||
|
# only ever holds ciphertext, which is what makes a publicly-reachable
|
||||||
|
# credentialed database an acceptable trade rather than a bad one.
|
||||||
|
#
|
||||||
|
# Its passphrase is a SEPARATE secret from couchdb_admin_password below —
|
||||||
|
# deliberately, and it must stay that way. The couchdb password
|
||||||
|
# authenticates to this server and is stored here (hashed) and in
|
||||||
|
# secrets/jupiter.yaml; the E2EE passphrase never leaves the Obsidian
|
||||||
|
# clients and CouchDB has no idea it exists. Reusing one string for both
|
||||||
|
# hands whoever obtains that credential the decryption key as well, which
|
||||||
|
# is precisely the failure E2EE is here to prevent. The passphrase is
|
||||||
|
# therefore NOT in sops (nothing on this host consumes it) — it lives in
|
||||||
|
# the HomeLab Proton Pass vault, with the deploy credentials.
|
||||||
|
#
|
||||||
|
# Losing it costs the remote database, not the notes: wipe it and
|
||||||
|
# re-initialize from a device that still holds the plaintext vault.
|
||||||
|
{
|
||||||
|
services.couchdb = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
# Listens on all interfaces, same reasoning as immich: :5984 is NOT opened
|
||||||
|
# in the firewall, so it is reachable over tailscale0 (trusted in
|
||||||
|
# common.nix) and localhost only. That is the path neptun's caddy takes.
|
||||||
|
bindAddress = "0.0.0.0";
|
||||||
|
port = 5984;
|
||||||
|
|
||||||
|
# The vault database is the ONLY copy of the notes once LiveSync is the
|
||||||
|
# source of truth, so it belongs on the array, not the 29G eMMC. All three
|
||||||
|
# of these default under /var/lib/couchdb and have to move together —
|
||||||
|
# configFile especially, since CouchDB writes to it at runtime (below).
|
||||||
|
databaseDir = "/mnt/data/AppData/couchdb";
|
||||||
|
viewIndexDir = "/mnt/data/AppData/couchdb";
|
||||||
|
configFile = "/mnt/data/AppData/couchdb/local.ini";
|
||||||
|
|
||||||
|
# The admin password, as an [admins] ini fragment from sops.
|
||||||
|
# services.couchdb.adminPass would render it into the world-readable
|
||||||
|
# store; extraConfigFiles is the module's own documented hook for this
|
||||||
|
# (hosts/jupiter/secrets.nix renders the template).
|
||||||
|
#
|
||||||
|
# ⚠️ CouchDB hashes a plaintext admin password at startup and persists the
|
||||||
|
# hash to the LAST, writable file in its ini chain — local.ini above,
|
||||||
|
# which then takes precedence over this fragment. So changing the sops
|
||||||
|
# value alone does NOT rotate the password: delete the `[admins]` line
|
||||||
|
# from /mnt/data/AppData/couchdb/local.ini and restart as well.
|
||||||
|
extraConfigFiles = [ config.sops.templates."couchdb-admins.ini".path ];
|
||||||
|
|
||||||
|
# Values taken from LiveSync's own CouchDB setup documentation; the plugin
|
||||||
|
# refuses to replicate (or silently truncates) without them.
|
||||||
|
extraConfig = {
|
||||||
|
couchdb = {
|
||||||
|
# Creates _users/_replicator on first boot instead of leaving the node
|
||||||
|
# in the un-set-up state where every request 500s.
|
||||||
|
single_node = "true";
|
||||||
|
# LiveSync splits notes into chunks, but a big pasted image still
|
||||||
|
# arrives as one document. 8MB (the default) is too small.
|
||||||
|
max_document_size = "50000000";
|
||||||
|
};
|
||||||
|
|
||||||
|
chttpd = {
|
||||||
|
require_valid_user = "true";
|
||||||
|
max_http_request_size = "4294967296";
|
||||||
|
enable_cors = "true";
|
||||||
|
};
|
||||||
|
|
||||||
|
chttpd_auth = {
|
||||||
|
require_valid_user = "true";
|
||||||
|
authentication_redirect = "/_utils/session.html";
|
||||||
|
};
|
||||||
|
|
||||||
|
httpd = {
|
||||||
|
# Makes CouchDB answer 401 with a WWW-Authenticate challenge rather
|
||||||
|
# than a bare 401 body — the plugin's basic-auth flow depends on it.
|
||||||
|
"WWW-Authenticate" = ''Basic realm="couchdb"'';
|
||||||
|
enable_cors = "true";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Obsidian is an Electron/Capacitor app, so its requests carry these
|
||||||
|
# non-http origins. Without them desktop and mobile both fail CORS
|
||||||
|
# preflight and the plugin reports a bare "cannot connect".
|
||||||
|
cors = {
|
||||||
|
credentials = "true";
|
||||||
|
origins = "app://obsidian.md,capacitor://localhost,http://localhost";
|
||||||
|
headers = "accept, authorization, content-type, origin, referer";
|
||||||
|
methods = "GET, PUT, POST, HEAD, DELETE";
|
||||||
|
max_age = "3600";
|
||||||
|
};
|
||||||
|
|
||||||
|
# The module points [log] file at /var/log/couchdb.log, which nothing
|
||||||
|
# rotates — on a 29G eMMC an info-level log of every replication request
|
||||||
|
# is a slow disk-fill. stderr hands it to journald's capped storage
|
||||||
|
# instead (the file setting is then ignored).
|
||||||
|
log = {
|
||||||
|
writer = "stderr";
|
||||||
|
level = "warning";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# /mnt/data/AppData is drwx--x--- darman:users, so the couchdb user needs
|
||||||
|
# group "users" just to traverse into its own database dir. The dir itself
|
||||||
|
# is created couchdb:couchdb by the module's tmpfiles rule.
|
||||||
|
users.users.couchdb.extraGroups = [ "users" ];
|
||||||
|
|
||||||
|
# databaseDir is outside /var/lib, so systemd derives no mount dependency
|
||||||
|
# from it. Without this CouchDB starts with the array missing, creates an
|
||||||
|
# empty database on the eMMC, and LiveSync sees a remote vault that lost
|
||||||
|
# every note — which it would then happily replicate back to the clients.
|
||||||
|
systemd.services.couchdb.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user