chore: baseline codex hardening before lifecycle refactor

Snapshots the in-flight hardening work so subsequent reconcile/Quadlet
phases land on a clean before/after diff.

Changes:
- core/container/src/podman_client.rs: image_uses_insecure_registry()
  whitelist for the OVH (146.59.87.168:3000) and legacy Hetzner
  (23.182.128.160:3000) HTTP mirrors; podman_network_settings() lifts
  custom networks into the Networks map so containers can join them.
- core/archipelago/src/container/prod_orchestrator.rs:
  ensure_container_network() creates per-manifest networks on demand;
  apply_data_uid() now goes through host_sudo for mkdir -p + chown so
  bind-mount roots get created and chowned without password prompts.
- core/archipelago/src/api/rpc/package/{install,update,stacks}.rs:
  podman pull adds --tls-verify=false only for whitelisted registries.
- core/archipelago/src/bootstrap.rs: removes stale dev-mode systemd
  override on startup (live nodes carried it from old installers).
- core/archipelago/src/config.rs: ignore ARCHIPELAGO_DEV_MODE in prod
  binaries — it had been silently rerouting volumes to /tmp.
- apps/bitcoin-{core,knots}/manifest.yml: locate bitcoind at runtime
  so image-layout differences don't break entrypoint.
- scripts/app-catalog-image-smoke-test.py: production catalog/image
  smoke test that probes a target node before users click Install.
- .gitignore: cover .codex, .pnpm-store, __pycache__, *.bak.

Removes filebrowser.rs.bak and two stale catalog.json.bak files
(verified identical to live counterparts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-05-01 08:52:29 -04:00
co-authored by Claude Opus 4.7
parent 05e6c2e738
commit 0684491072
12 changed files with 439 additions and 42 deletions
+3 -1
View File
@@ -14,6 +14,8 @@ pub use manifest::{
ManifestError, ResolvedSource, ResourceLimits, SecretEnv, SecretsProvider, SecurityPolicy,
Volume,
};
pub use podman_client::{ContainerState, ContainerStatus, PodmanClient};
pub use podman_client::{
image_uses_insecure_registry, ContainerState, ContainerStatus, PodmanClient,
};
pub use port_manager::{PortError, PortManager};
pub use runtime::{AutoRuntime, ContainerRuntime, DockerRuntime, PodmanRuntime};
+73 -14
View File
@@ -257,7 +257,11 @@ impl PodmanClient {
pub async fn pull_image(&self, image: &str, _signature: Option<&str>) -> Result<()> {
// Image pull uses CLI — it's a streaming operation that the API handles differently
let mut cmd = tokio::process::Command::new("podman");
cmd.arg("pull").arg(image);
cmd.arg("pull");
if image_uses_insecure_registry(image) {
cmd.arg("--tls-verify=false");
}
cmd.arg(image);
let output = tokio::time::timeout(
std::time::Duration::from_secs(600), // 10 min for large images
@@ -357,20 +361,12 @@ impl PodmanClient {
);
}
let net_mode = if let Some(n) = manifest.app.container.network.as_ref() {
if n.is_empty() {
"bridge"
} else {
n.as_str()
}
} else {
match manifest.app.security.network_policy.as_str() {
"host" => "host",
_ => "bridge",
}
};
let (net_mode, custom_network) = podman_network_settings(
manifest.app.container.network.as_deref(),
manifest.app.security.network_policy.as_str(),
);
let body = serde_json::json!({
let mut body = serde_json::json!({
"name": name,
"image": image_ref,
"portmappings": port_mappings,
@@ -393,6 +389,11 @@ impl PodmanClient {
"nsmode": net_mode
},
});
if let Some(network) = custom_network {
body.as_object_mut()
.expect("container create body is a JSON object")
.insert("networks".to_string(), serde_json::json!({ network: {} }));
}
let result = self
.api_request("POST", "libpod/containers/create", Some(body), LONG_TIMEOUT)
@@ -601,6 +602,30 @@ impl PodmanClient {
}
}
pub fn image_uses_insecure_registry(image: &str) -> bool {
matches!(
image.split('/').next(),
Some("146.59.87.168:3000") | Some("23.182.128.160:3000")
)
}
fn podman_network_settings(
network: Option<&str>,
network_policy: &str,
) -> (&'static str, Option<String>) {
match network {
Some("") => ("bridge", None),
Some("host") => ("host", None),
Some("bridge") => ("bridge", None),
Some("none") => ("none", None),
Some("slirp4netns") => ("slirp4netns", None),
Some("private") => ("private", None),
Some(custom) => ("bridge", Some(custom.to_string())),
None if network_policy == "host" => ("host", None),
None => ("bridge", None),
}
}
// ─── Helpers ─────────────────────────────────────────────────────
fn parse_port_bindings(bindings: &serde_json::Value) -> Vec<String> {
@@ -673,6 +698,40 @@ fn parse_memory_limit(limit: &str) -> Option<i64> {
mod tests {
use super::*;
#[test]
fn insecure_registry_detection_matches_http_mirrors_only() {
assert!(image_uses_insecure_registry(
"146.59.87.168:3000/lfg2025/bitcoin-knots:latest"
));
assert!(image_uses_insecure_registry(
"23.182.128.160:3000/lfg2025/filebrowser:v2.27.0"
));
assert!(!image_uses_insecure_registry(
"git.tx1138.com/lfg2025/bitcoin-knots:latest"
));
assert!(!image_uses_insecure_registry(
"docker.io/library/nginx:latest"
));
}
#[test]
fn podman_network_settings_uses_networks_map_for_custom_networks() {
assert_eq!(
podman_network_settings(Some("archy-net"), "isolated"),
("bridge", Some("archy-net".to_string()))
);
assert_eq!(
podman_network_settings(Some("host"), "isolated"),
("host", None)
);
assert_eq!(
podman_network_settings(Some(""), "isolated"),
("bridge", None)
);
assert_eq!(podman_network_settings(None, "host"), ("host", None));
assert_eq!(podman_network_settings(None, "isolated"), ("bridge", None));
}
#[test]
fn parse_memory_limit_iec_binary_suffixes() {
// Kubernetes-style — this is what apps/*/manifest.yml uses.