diff --git a/core/archipelago/src/fips/app_ports.rs b/core/archipelago/src/fips/app_ports.rs new file mode 100644 index 00000000..2f8d25e5 --- /dev/null +++ b/core/archipelago/src/fips/app_ports.rs @@ -0,0 +1,44 @@ +//! Generated by scripts/generate-app-catalog.py. Do not edit manually. +//! +//! Catalog app launch ports (the web UIs the companion opens by direct +//! port). Used to write the fips0 firewall allowance drop-in so app UIs +//! are reachable over the mesh; ports of apps that aren't installed have +//! no listener, so allowing them is inert. + +pub const APP_LAUNCH_PORTS: &[u16] = &[ + 2283, + 2342, + 3000, + 3001, + 3002, + 4080, + 5180, + 7778, + 8080, + 8081, + 8082, + 8083, + 8084, + 8085, + 8087, + 8088, + 8089, + 8090, + 8096, + 8123, + 8175, + 8176, + 8240, + 8334, + 8888, + 8999, + 9000, + 9100, + 10380, + 11434, + 18081, + 18083, + 23000, + 32838, + 50002, +]; diff --git a/core/archipelago/src/fips/config.rs b/core/archipelago/src/fips/config.rs index 29112ac1..3dff85ce 100644 --- a/core/archipelago/src/fips/config.rs +++ b/core/archipelago/src/fips/config.rs @@ -252,6 +252,29 @@ pub async fn install(identity_dir: &Path) -> Result<()> { let nft_install = sudo_install_file(&nft_stage, "/etc/fips/fips.d/80-web-ui.nft", "0644").await; let _ = tokio::fs::remove_file(&nft_stage).await; nft_install?; + + // App launch ports: the companion opens catalog apps by direct port + // over the mesh. Ports of apps that aren't installed have no listener, + // so the allowance is inert until an app exists to answer. + let port_list = super::app_ports::APP_LAUNCH_PORTS + .iter() + .map(|p| p.to_string()) + .collect::>() + .join(", "); + let app_dropin = format!( + "# Written by archipelago on every daemon config install.\n\ + # Catalog app launch ports (web UIs) allowed through the fips0\n\ + # default-deny inbound baseline. Service/RPC ports stay closed.\n\ + tcp dport {{ {port_list} }} accept\n" + ); + let app_stage = std::env::temp_dir().join(format!("fips-appports-{}.nft", std::process::id())); + tokio::fs::write(&app_stage, app_dropin) + .await + .context("Failed to stage app-ports nft drop-in")?; + let app_install = + sudo_install_file(&app_stage, "/etc/fips/fips.d/85-app-ports.nft", "0644").await; + let _ = tokio::fs::remove_file(&app_stage).await; + app_install?; // Make the allowance live immediately; a no-op error when the // hardening baseline isn't installed on this node yet. if tokio::fs::try_exists("/etc/fips/fips.nft").await.unwrap_or(false) { diff --git a/core/archipelago/src/fips/mod.rs b/core/archipelago/src/fips/mod.rs index bbbcfa38..e8a26511 100644 --- a/core/archipelago/src/fips/mod.rs +++ b/core/archipelago/src/fips/mod.rs @@ -26,6 +26,7 @@ #![allow(dead_code)] pub mod anchors; +pub mod app_ports; pub mod config; pub mod dial; pub mod iface; diff --git a/scripts/generate-app-catalog.py b/scripts/generate-app-catalog.py index 6ee7a96e..09445b0b 100644 --- a/scripts/generate-app-catalog.py +++ b/scripts/generate-app-catalog.py @@ -141,6 +141,32 @@ def render_app_session_config(manifests: dict[str, dict[str, Any]]) -> str: return "\n".join(lines) +def render_rust_ports(ports: dict[str, int], extra_ports: list[int]) -> str: + """Rust constant of catalog launch ports for the fips0 firewall drop-in + (core/archipelago/src/fips/app_ports.rs). Extra ports cover the frontend's + APP_PORTS overrides (companions/aliases) that have no manifest of their own. + """ + distinct = sorted(set(list(ports.values()) + extra_ports)) + lines = [ + "//! Generated by scripts/generate-app-catalog.py. Do not edit manually.", + "//!", + "//! Catalog app launch ports (the web UIs the companion opens by direct", + "//! port). Used to write the fips0 firewall allowance drop-in so app UIs", + "//! are reachable over the mesh; ports of apps that aren\'t installed have", + "//! no listener, so allowing them is inert.", + "", + "pub const APP_LAUNCH_PORTS: &[u16] = &[", + ] + lines.extend(f" {port}," for port in distinct) + lines.extend(["];", ""]) + return "\n".join(lines) + + +# Keep in lockstep with APP_PORTS overrides in +# neode-ui/src/views/appSession/appSessionConfig.ts. +RUST_EXTRA_PORTS = [8334, 50002, 18083, 11434, 8081, 8240, 8175, 8176, 8080] + + def sync_catalog(path: Path, manifests: dict[str, dict[str, Any]]) -> int: with path.open("r", encoding="utf-8") as fh: catalog = json.load(fh) @@ -178,6 +204,11 @@ def main() -> int: default=[], help="Catalog JSON path to update. May be passed multiple times.", ) + parser.add_argument( + "--rust-app-ports", + default="core/archipelago/src/fips/app_ports.rs", + help="Generated Rust launch-port list for the fips0 firewall drop-in. Empty string to skip.", + ) parser.add_argument( "--app-session-config", default="neode-ui/src/views/appSession/generatedAppSessionConfig.ts", @@ -201,6 +232,20 @@ def main() -> int: print(f"{path}: updated") else: print(f"{path}: updated 0 fields") + if args.rust_app_ports: + ports = { + app_id: port + for app_id, app in manifests.items() + if (port := manifest_launch_port(app)) + } + rust_path = Path(args.rust_app_ports) + rust_content = render_rust_ports(ports, RUST_EXTRA_PORTS) + rust_old = rust_path.read_text(encoding="utf-8") if rust_path.exists() else "" + if rust_old != rust_content: + rust_path.write_text(rust_content, encoding="utf-8") + print(f"{rust_path}: updated") + else: + print(f"{rust_path}: updated 0 fields") print(f"total_updated={total}") return 0