fix(node): run the TTL sweep in-process; drop the broken external sweep timer
Turso locks the datastore exclusively, so a separate `buh-cli sweep` (and the
shipped systemd sweep timer) cannot open the DB while the daemon holds it — they
fail with a file-locking error. Move the sweep into the daemon as a periodic
in-process task (`[relay].sweep_interval_seconds`, default 3600s), running in
both plain and PQ-mTLS modes.
Remove asset/systemd/buh-node-sweep.{service,timer} and update deploy.sh,
manifest.yml, the config template, and the readme accordingly. The readme also
notes the broader constraint: CLI trust/admin commands cannot run against a live
daemon's datastore — a daemon-side admin path is the next step.
cargo check/fmt/clippy(--all-features) clean; in-daemon sweep verified live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EB3LjarCdXxqrJ4tFLn8LB
This commit is contained in:
@@ -18,6 +18,7 @@ max_ttl_seconds = {{MAX_TTL_SECONDS}}
|
||||
max_payload_bytes = {{MAX_PAYLOAD_BYTES}}
|
||||
max_pull_limit = {{MAX_PULL_LIMIT}}
|
||||
max_wait_seconds = {{MAX_WAIT_SECONDS}}
|
||||
sweep_interval_seconds = {{SWEEP_INTERVAL_SECONDS}} # in-process TTL sweep (no external timer)
|
||||
|
||||
[blob]
|
||||
# A node opts into the blob role. `fs` stores opaque client-encrypted ciphertext on local disk
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Deploy one buh node on the local host (run as root on the target).
|
||||
#
|
||||
# buh has no central control plane, so this installs a single self-contained node: the binaries,
|
||||
# a hardened systemd service, the TTL-sweep timer, the firewalld opening for BUH_NODE_PORT, and a
|
||||
# a hardened systemd service, the firewalld opening for BUH_NODE_PORT, and a
|
||||
# rendered /etc/buh/config.toml. It then has the node generate its own CA and prints the
|
||||
# fingerprint to share with peers. There is deliberately NO step-ca, NO central database
|
||||
# bootstrap, and NO secret material to provision.
|
||||
@@ -37,6 +37,7 @@ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
: "${MAX_PAYLOAD_BYTES:=262144}"
|
||||
: "${MAX_PULL_LIMIT:=100}"
|
||||
: "${MAX_WAIT_SECONDS:=30}"
|
||||
: "${SWEEP_INTERVAL_SECONDS:=3600}"
|
||||
|
||||
: "${BLOB_ENABLED:=true}"
|
||||
: "${BLOB_BACKEND:=fs}"
|
||||
@@ -74,6 +75,7 @@ render() {
|
||||
-e "s|{{MAX_PAYLOAD_BYTES}}|${MAX_PAYLOAD_BYTES}|g" \
|
||||
-e "s|{{MAX_PULL_LIMIT}}|${MAX_PULL_LIMIT}|g" \
|
||||
-e "s|{{MAX_WAIT_SECONDS}}|${MAX_WAIT_SECONDS}|g" \
|
||||
-e "s|{{SWEEP_INTERVAL_SECONDS}}|${SWEEP_INTERVAL_SECONDS}|g" \
|
||||
-e "s|{{BLOB_ENABLED}}|${BLOB_ENABLED}|g" \
|
||||
-e "s|{{BLOB_BACKEND}}|${BLOB_BACKEND}|g" \
|
||||
-e "s|{{BLOB_FS_ROOT}}|${BLOB_FS_ROOT}|g" \
|
||||
@@ -94,10 +96,8 @@ render "${CONFIG_DIR}/config.toml"
|
||||
chmod 0640 "${CONFIG_DIR}/config.toml"
|
||||
chgrp buh "${CONFIG_DIR}/config.toml"
|
||||
|
||||
echo "==> Installing systemd units"
|
||||
echo "==> Installing systemd unit"
|
||||
install -m 0644 "${here}/systemd/buh-node.service" /etc/systemd/system/buh-node.service
|
||||
install -m 0644 "${here}/systemd/buh-node-sweep.service" /etc/systemd/system/buh-node-sweep.service
|
||||
install -m 0644 "${here}/systemd/buh-node-sweep.timer" /etc/systemd/system/buh-node-sweep.timer
|
||||
systemctl daemon-reload
|
||||
|
||||
if [[ "${PKI_ENABLED}" == "true" ]]; then
|
||||
@@ -111,9 +111,8 @@ if [[ "${PKI_ENABLED}" == "true" ]]; then
|
||||
runuser -u buh -- "${BIN_DIR}/buh-cli" --db-path "${DB_PATH}" --pki-dir "${PKI_DIR}" ca init
|
||||
fi
|
||||
|
||||
echo "==> Enabling services"
|
||||
echo "==> Enabling the node service"
|
||||
systemctl enable --now buh-node.service
|
||||
systemctl enable --now buh-node-sweep.timer
|
||||
|
||||
echo
|
||||
echo "buh node deployed."
|
||||
|
||||
@@ -24,6 +24,7 @@ environments:
|
||||
max_payload_bytes: 262144 # 256 KiB
|
||||
max_pull_limit: 100
|
||||
max_wait_seconds: 30
|
||||
sweep_interval_seconds: 3600 # in-process TTL sweep (no external timer)
|
||||
blob:
|
||||
enabled: true
|
||||
backend: fs # filesystem/ZFS; opaque client-encrypted ciphertext only
|
||||
|
||||
@@ -10,8 +10,9 @@ plane, no shared database, and no central PKI**:
|
||||
is an embedded Turso file and the node generates its own CA.
|
||||
- `systemd/buh-node.service` — hardened unit (`generic.md` §8). **No `*.path` cert-reload unit**:
|
||||
the node is its own CA and rotates its TLS leaf *in process* — that is the decentralised-CA
|
||||
deviation. Nothing external watches or reloads a certificate.
|
||||
- `systemd/buh-node-sweep.{service,timer}` — periodic TTL sweep of expired envelopes.
|
||||
deviation. Nothing external watches or reloads a certificate. The TTL sweep also runs *in
|
||||
process* (`[relay].sweep_interval_seconds`), because Turso locks the datastore exclusively — a
|
||||
separate `buh-cli sweep` cannot open the DB while the daemon holds it.
|
||||
- `systemd/buh.sysusers.conf` — the unprivileged `buh` service account.
|
||||
- `firewalld/buh-node.xml` — opens **`BUH_NODE_PORT` (8443)**, the single PQ-mTLS ingress port.
|
||||
The plain loopback health port is never exposed.
|
||||
@@ -33,6 +34,12 @@ sudo PKI_SANS='["node1.example.com"]' ./asset/deploy.sh
|
||||
|
||||
Each node pins peers by CA fingerprint — there is no shared root.
|
||||
|
||||
> **Note (PoC limitation):** Turso locks the datastore exclusively, so these CLI commands cannot
|
||||
> run while this host's `buh-api` daemon is up. Stop the service to change trust
|
||||
> (`systemctl stop buh-node` → edit trust → `systemctl start buh-node`), or run them from an
|
||||
> operator workstation against a copy. A daemon-side admin path that removes this constraint is
|
||||
> the next planned step.
|
||||
|
||||
```sh
|
||||
buh-cli ca show # print my fingerprint to hand to a peer
|
||||
buh-cli peer trust <their-ca-fp> # accept that peer over PQ-mTLS
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
[Unit]
|
||||
Description=buh TTL sweep — delete expired envelopes from the relay datastore
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=buh
|
||||
Group=buh
|
||||
ExecStart=/usr/local/bin/buh-cli --db-path /var/lib/buh/relay.db sweep
|
||||
|
||||
# Same hardening posture as the node service; only the datastore is writable.
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
SystemCallArchitectures=native
|
||||
ReadWritePaths=/var/lib/buh
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
@@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
Description=Periodic buh TTL sweep of expired envelopes
|
||||
|
||||
[Timer]
|
||||
# Envelopes also expire lazily on read; this sweep reclaims space for queues that are never
|
||||
# pulled again. Hourly is ample for a store-and-forward relay.
|
||||
OnCalendar=hourly
|
||||
Persistent=true
|
||||
RandomizedDelaySec=5m
|
||||
Unit=buh-node-sweep.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -82,6 +82,10 @@ pub struct RelayConfig {
|
||||
pub max_pull_limit: i64,
|
||||
/// Maximum long-poll wait a client may request, in seconds.
|
||||
pub max_wait_seconds: u64,
|
||||
/// How often the daemon sweeps expired envelopes, in seconds. The sweep runs **in process**
|
||||
/// because Turso locks the datastore exclusively — a separate `buh-cli sweep` cannot run while
|
||||
/// the daemon holds the DB.
|
||||
pub sweep_interval_seconds: u64,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
@@ -97,6 +101,7 @@ impl Default for AppConfig {
|
||||
max_payload_bytes: core.max_payload_bytes,
|
||||
max_pull_limit: core.max_pull_limit,
|
||||
max_wait_seconds: 30,
|
||||
sweep_interval_seconds: 3600,
|
||||
},
|
||||
blob: BlobConfig {
|
||||
enabled: false,
|
||||
|
||||
@@ -15,7 +15,7 @@ use tracing_subscriber::EnvFilter;
|
||||
|
||||
use buh_api::config::{AppConfig, BlobConfig};
|
||||
use buh_api::router::router;
|
||||
use buh_api::serve::{serve_plain, serve_pqmtls};
|
||||
use buh_api::serve::{serve_plain, serve_pqmtls, spawn_sweeper};
|
||||
use buh_api::state::AppState;
|
||||
use buh_api::tls::{NodeTls, TrustStore};
|
||||
use buh_data::DataStack;
|
||||
@@ -57,6 +57,12 @@ async fn main() -> anyhow::Result<()> {
|
||||
)?;
|
||||
}
|
||||
|
||||
// In-process TTL sweep (an external `buh-cli sweep` cannot run while the daemon holds the DB).
|
||||
spawn_sweeper(
|
||||
stack.ctx.clone(),
|
||||
Duration::from_secs(config.relay.sweep_interval_seconds),
|
||||
);
|
||||
|
||||
let state = AppState {
|
||||
ctx: stack.ctx.clone(),
|
||||
max_wait: Duration::from_secs(config.relay.max_wait_seconds),
|
||||
|
||||
@@ -15,10 +15,31 @@ use hyper_util::service::TowerToHyperService;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
|
||||
use buh_core::PeerTrustRegistry;
|
||||
use buh_core::{Ctx, PeerTrustRegistry, mailbox};
|
||||
|
||||
use crate::tls::{NodeTls, TrustStore};
|
||||
|
||||
/// Spawn the in-process TTL sweep that deletes expired envelopes every `interval`.
|
||||
///
|
||||
/// The sweep runs inside the daemon — not as an external `buh-cli sweep` — because Turso locks the
|
||||
/// datastore exclusively, so a second process cannot open the DB while the daemon holds it.
|
||||
pub fn spawn_sweeper(ctx: Ctx, interval: Duration) {
|
||||
let interval = interval.max(Duration::from_secs(1));
|
||||
tracing::info!(interval_secs = interval.as_secs(), "TTL sweeper started");
|
||||
tokio::spawn(async move {
|
||||
let mut tick = tokio::time::interval(interval);
|
||||
tick.tick().await; // consume the immediate first tick
|
||||
loop {
|
||||
tick.tick().await;
|
||||
match mailbox::sweep(&ctx).await {
|
||||
Ok(0) => {}
|
||||
Ok(n) => tracing::info!(removed = n, "swept expired envelopes"),
|
||||
Err(e) => tracing::error!(error = %e, "TTL sweep failed"),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Plain-HTTP ingress: the dev/web-demo loopback mode. No certificates.
|
||||
pub async fn serve_plain(
|
||||
app: axum::Router,
|
||||
|
||||
Reference in New Issue
Block a user