fix(appgate+container): stop stripping app cookies; create named volumes correctly

Two daemon bugs, one debugging arc (2026-08-05, operator-reported):

1. The app gate removed the ENTIRE Cookie header before proxying. That
   broke the data plane of every first-party companion UI behind the gate
   (lnd-ui/bitcoin-ui/electrs-ui/fips-ui render their shell, then every
   /proxy/* and /lnd-connect-info call 401s — observed as "LND UI
   unreachable"), and silently logged users out of every gated app with
   its own cookie login (vaultwarden, nextcloud, gitea) on each request.
   The gate now strips only its own cookie pairs (session, csrf_token);
   a new per-port manifest opt-in `session_passthrough: true` forwards
   the node session to first-party UIs whose nginx proxies the daemon's
   authenticated endpoints. Undeclared ports never get passthrough.

2. podman_client::create_container sent named volumes to the libpod API
   as bind mounts with the bare volume name as source, so creating any
   manifest app with a `type: volume` mount failed. On .38 the reconciler
   removed indeedhub-postgres/-minio for env drift and then could never
   create their replacements, leaving the stack half-missing forever.
   Named volumes now ride the spec's `volumes` field ({Name, Dest,
   Options}). Also: the reconcile-failure log now prints the full anyhow
   chain — `%e` showed only "create_container X" and hid the real error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-05 21:21:52 -04:00
co-authored by Claude Fable 5
parent 4ace62fad9
commit ada59acdd5
5 changed files with 133 additions and 7 deletions
+14
View File
@@ -599,6 +599,19 @@ pub struct PortMapping {
/// means the author expected an exemption they did not get.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth_rationale: Option<String>,
/// Forward the node session cookie to the app on authorised requests.
///
/// The gate normally strips its own credential before proxying — an app
/// must never be in a position to log or replay the node session. The
/// first-party companion UIs (lnd-ui, bitcoin-ui, electrs-ui, fips-ui)
/// are the exception their design requires: their nginx forwards the
/// browser's session cookie to the daemon's authenticated endpoints
/// (`/proxy/lnd/*`, `/rpc/v1`, `/lnd-connect-info`), so stripping it
/// breaks every data call behind the gate with a 401 while the page
/// shell still renders (observed as "LND UI unreachable", 2026-08-05).
/// Only meaningful on a `auth: gated` port.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub session_passthrough: bool,
}
impl PortMapping {
@@ -626,6 +639,7 @@ impl From<(u16, u16)> for PortMapping {
bind: String::new(),
auth: None,
auth_rationale: None,
session_passthrough: false,
}
}
}
+15
View File
@@ -366,6 +366,7 @@ impl PodmanClient {
}
let mut mounts = Vec::new();
let mut named_volumes = Vec::new();
for volume in &manifest.app.volumes {
if volume.volume_type == "tmpfs" {
let options: Vec<String> = volume
@@ -382,6 +383,19 @@ impl PodmanClient {
"type": "tmpfs",
"options": options,
}));
} else if volume.volume_type == "volume" {
// Named podman volume. The libpod create spec carries these in
// the separate `volumes` field ({Name, Dest, Options}), NOT in
// `mounts`: sending one as a bind mount makes the API treat
// the bare volume name as a host path and the create fails —
// which left indeedhub-postgres/-minio permanently absent on
// legacy-path nodes (the reconciler removed the old container
// for drift, then could never create its replacement).
named_volumes.push(serde_json::json!({
"Name": volume.source,
"Dest": volume.target,
"Options": volume.options,
}));
} else {
mounts.push(serde_json::json!({
"destination": volume.target,
@@ -464,6 +478,7 @@ impl PodmanClient {
"image": image_ref,
"portmappings": port_mappings,
"mounts": mounts,
"volumes": named_volumes,
"env": env_map,
"secret_env": secret_env_map,
"labels": labels_map,