2026-03-04 05:23:42 +00:00
use super ::RpcHandler ;
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
use crate ::data_model ::{
Description , InstallProgress , Manifest , PackageDataEntry , PackageState , StaticFiles ,
} ;
2026-02-17 15:03:34 +00:00
use crate ::port_allocator ::PortAllocator ;
2026-01-24 22:59:20 +00:00
use anyhow ::{ Context , Result } ;
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
use tokio ::io ::{ AsyncBufReadExt , BufReader } ;
feat: cloud native file browser, settings Claude auth, deploy hardening
- Add native Cloud file browser with FileBrowser API integration
- Add cloud store, filebrowser-client, useAudioPlayer, useFileType composables
- Add Cloud components: FileGrid, FileCard, FileCardGrid, CloudToolbar
- Add Claude authentication section to Settings with OAuth status check
- Harden deploy script to preserve /aiui/ and claude-login.html
- Add nginx proxies for btcpay, homeassistant, filebrowser (HTTPS block)
- Add app configs for filebrowser, searxng, penpot in package.rs
- Update goal progress tracking with app aliases
- Improve mobile back button composable with ResizeObserver
- Update various views with cloud integration and UI refinements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:05:01 +00:00
use tracing ::{ debug , info } ;
2026-01-24 22:59:20 +00:00
impl RpcHandler {
2026-02-01 18:46:35 +00:00
/// Install a package from a Docker image
/// Security: Image verification, resource limits, network isolation
2026-03-04 05:23:42 +00:00
pub ( super ) async fn handle_package_install (
2026-02-01 18:46:35 +00:00
& self ,
params : Option < serde_json ::Value > ,
) -> Result < serde_json ::Value > {
let params = params . ok_or_else ( | | anyhow ::anyhow! ( " Missing params " ) ) ? ;
2026-03-04 05:23:42 +00:00
2026-02-01 18:46:35 +00:00
let package_id = params
. get ( " id " )
. and_then ( | v | v . as_str ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " Missing package id " ) ) ? ;
2026-03-06 03:26:56 +00:00
validate_app_id ( package_id ) ? ;
2026-03-04 05:23:42 +00:00
2026-02-01 18:46:35 +00:00
let docker_image = params
. get ( " dockerImage " )
. and_then ( | v | v . as_str ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " Missing dockerImage " ) ) ? ;
debug! ( " Installing package {} from image {} " , package_id , docker_image ) ;
// Security: Validate image name format (prevent injection)
if ! is_valid_docker_image ( docker_image ) {
return Err ( anyhow ::anyhow! ( " Invalid Docker image format " ) ) ;
}
2026-03-01 17:53:18 +00:00
// Virtual app: Indeehub (no container, opens external URL)
if package_id = = " indeedhub " {
return Ok ( serde_json ::json! ( { " success " : true } ) ) ;
}
2026-02-25 18:04:41 +00:00
// Multi-container apps: create full stack
if package_id = = " immich " {
return self . install_immich_stack ( ) . await ;
}
if package_id = = " penpot " | | package_id = = " penpot-frontend " {
return self . install_penpot_stack ( ) . await ;
}
2026-03-09 00:33:03 +00:00
// Dependency checks: verify required services are running before install
2026-03-09 07:43:12 +00:00
let has_lnd ;
2026-03-09 00:33:03 +00:00
{
let dep_check = tokio ::process ::Command ::new ( " sudo " )
2026-03-08 02:16:02 +00:00
. args ( [ " podman " , " ps " , " --format " , " {{.Names}} " ] )
. output ( )
. await
. context ( " Failed to check running containers " ) ? ;
2026-03-09 00:33:03 +00:00
let running = String ::from_utf8_lossy ( & dep_check . stdout ) ;
let is_running = | names : & [ & str ] | {
running . lines ( ) . any ( | l | {
let name = l . trim ( ) ;
names . iter ( ) . any ( | n | name = = * n )
} )
} ;
let has_bitcoin = is_running ( & [ " bitcoin-knots " , " bitcoin-core " , " bitcoin " ] ) ;
let has_electrs = is_running ( & [ " mempool-electrs " , " electrs " ] ) ;
2026-03-09 07:43:12 +00:00
has_lnd = is_running ( & [ " lnd " ] ) ;
2026-03-09 00:33:03 +00:00
match package_id {
" mempool-electrs " | " electrs " if ! has_bitcoin = > {
return Err ( anyhow ::anyhow! (
" Electrs requires a running Bitcoin node (Bitcoin Knots). Please install and start Bitcoin Knots first. "
) ) ;
}
" lnd " if ! has_bitcoin = > {
return Err ( anyhow ::anyhow! (
" LND requires a running Bitcoin node (Bitcoin Knots). Please install and start Bitcoin Knots first. "
) ) ;
}
" btcpay-server " | " btcpayserver " if ! has_bitcoin = > {
return Err ( anyhow ::anyhow! (
" BTCPay Server requires a running Bitcoin node (Bitcoin Knots). Please install and start Bitcoin Knots first. "
) ) ;
}
" mempool " | " mempool-web " if ! has_bitcoin | | ! has_electrs = > {
let mut missing = vec! [ ] ;
if ! has_bitcoin { missing . push ( " Bitcoin Knots " ) ; }
if ! has_electrs { missing . push ( " Electrs " ) ; }
return Err ( anyhow ::anyhow! (
" Mempool requires {} to be running. Please install and start {} first. " ,
missing . join ( " and " ) ,
missing . join ( " and " )
) ) ;
}
" fedimint " if ! has_bitcoin = > {
return Err ( anyhow ::anyhow! (
" Fedimint requires a running Bitcoin node (Bitcoin Knots). Please install and start Bitcoin Knots first. "
) ) ;
}
_ = > { }
}
// Log dependency info for apps that have optional deps
if matches! ( package_id , " btcpay-server " | " btcpayserver " ) & & ! has_lnd {
info! ( " BTCPay Server installing without LND — Lightning payments won't be available until LND is installed " ) ;
2026-03-08 02:16:02 +00:00
}
}
2026-02-01 18:46:35 +00:00
// Check if container already exists
let check_output = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " ps " , " -a " , " --format " , " {{.Names}} " , " --filter " , & format! ( " name=^ {} $ " , package_id ) ] )
. output ( )
. await
. context ( " Failed to check existing containers " ) ? ;
if ! String ::from_utf8_lossy ( & check_output . stdout ) . trim ( ) . is_empty ( ) {
return Err ( anyhow ::anyhow! ( " Container {} already exists. Stop and remove it first. " , package_id ) ) ;
}
2026-03-01 17:53:18 +00:00
// Pull the image (skip for local images - must be built locally first)
let is_local_image = docker_image . starts_with ( " localhost/ " ) ;
if ! is_local_image {
debug! ( " Pulling image: {} " , docker_image ) ;
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
// Set package state to Installing with progress
self . set_install_progress ( package_id , 0 , 0 ) . await ;
// Stream pull progress via piped stderr
let mut child = tokio ::process ::Command ::new ( " sudo " )
2026-03-01 17:53:18 +00:00
. args ( [ " podman " , " pull " , docker_image ] )
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
. stdout ( std ::process ::Stdio ::piped ( ) )
. stderr ( std ::process ::Stdio ::piped ( ) )
. spawn ( )
. context ( " Failed to start image pull " ) ? ;
// Parse stderr for progress updates
if let Some ( stderr ) = child . stderr . take ( ) {
let reader = BufReader ::new ( stderr ) ;
let mut lines = reader . lines ( ) ;
let pkg_id = package_id . to_string ( ) ;
let state_mgr = self . state_manager . clone ( ) ;
while let Ok ( Some ( line ) ) = lines . next_line ( ) . await {
// Podman outputs lines like: "Copying blob sha256:abc123 [=====> ] 50.0MiB / 100.0MiB"
// or "Getting image source signatures" etc.
if let Some ( ( downloaded , total ) ) = parse_pull_progress ( & line ) {
Self ::update_install_progress ( & state_mgr , & pkg_id , downloaded , total ) . await ;
}
}
}
2026-02-01 18:46:35 +00:00
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
let status = child . wait ( ) . await . context ( " Failed to wait for image pull " ) ? ;
if ! status . success ( ) {
self . clear_install_progress ( package_id ) . await ;
return Err ( anyhow ::anyhow! ( " Failed to pull image " ) ) ;
2026-03-01 17:53:18 +00:00
}
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
// Mark pull as complete (100%)
self . set_install_progress ( package_id , 100 , 100 ) . await ;
2026-03-01 17:53:18 +00:00
} else {
// Verify local image exists
let images_output = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " images " , " -q " , docker_image ] )
. output ( )
. await
. context ( " Failed to check local image " ) ? ;
if String ::from_utf8_lossy ( & images_output . stdout ) . trim ( ) . is_empty ( ) {
return Err ( anyhow ::anyhow! (
" Local image {} not found. Run ./deploy-to-archipelago.sh from the Indeehub Prototype project on your Mac—it builds the image on this server and starts the app. " ,
docker_image
) ) ;
}
debug! ( " Using local image: {} " , docker_image ) ;
2026-02-01 18:46:35 +00:00
}
2026-03-08 02:16:02 +00:00
// Normalize container name: "electrs" alias -> "mempool-electrs"
let container_name = if package_id = = " electrs " { " mempool-electrs " } else { package_id } ;
2026-02-01 18:46:35 +00:00
// Create and start container with security constraints
let mut run_args = vec! [
" podman " , " run " ,
" -d " , // Detached
2026-03-08 02:16:02 +00:00
" --name " , container_name ,
2026-02-01 18:46:35 +00:00
" --restart=unless-stopped " , // Auto-restart policy
] ;
// App-specific configuration (should come from manifest)
2026-03-09 07:43:12 +00:00
let ( mut ports , mut volumes , env_vars , custom_command , mut custom_args ) = {
2026-02-17 15:03:34 +00:00
let mut allocator = self . port_allocator . lock ( ) . map_err ( | e | {
anyhow ::anyhow! ( " Port allocator lock poisoned: {} " , e )
} ) ? ;
get_app_config ( package_id , & self . config . host_ip , & mut allocator )
} ;
2026-03-04 05:23:42 +00:00
2026-03-09 07:43:12 +00:00
// Fedimint Gateway: auto-detect LND and switch to lnd mode
if package_id = = " fedimint-gateway " & & has_lnd {
let lnd_cert = " /var/lib/archipelago/lnd/tls.cert " ;
let lnd_macaroon = " /var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon " ;
if std ::path ::Path ::new ( lnd_cert ) . exists ( ) & & std ::path ::Path ::new ( lnd_macaroon ) . exists ( ) {
info! ( " LND detected with credentials — configuring gateway in lnd mode " ) ;
// Remove LDK port (9737) since we'll use LND
ports . retain ( | p | p ! = " 9737:9737 " ) ;
// Mount LND credentials read-only
volumes . push ( format! ( " {} :/lnd/tls.cert:ro " , lnd_cert ) ) ;
volumes . push ( format! ( " {} :/lnd/admin.macaroon:ro " , lnd_macaroon ) ) ;
// Switch args from ldk to lnd
custom_args = Some ( vec! [
" gatewayd " . to_string ( ) ,
" --data-dir " . to_string ( ) , " /data " . to_string ( ) ,
" --listen " . to_string ( ) , " 0.0.0.0:8176 " . to_string ( ) ,
" --bcrypt-password-hash " . to_string ( ) ,
" $2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC " . to_string ( ) ,
" --network " . to_string ( ) , " bitcoin " . to_string ( ) ,
" --bitcoind-url " . to_string ( ) , format! ( " http:// {} :8332 " , self . config . host_ip ) ,
" --bitcoind-username " . to_string ( ) , " archipelago " . to_string ( ) ,
" --bitcoind-password " . to_string ( ) , " archipelago123 " . to_string ( ) ,
" lnd " . to_string ( ) ,
" --lnd-rpc-host " . to_string ( ) , format! ( " {} :10009 " , self . config . host_ip ) ,
" --lnd-tls-cert " . to_string ( ) , " /lnd/tls.cert " . to_string ( ) ,
" --lnd-macaroon " . to_string ( ) , " /lnd/admin.macaroon " . to_string ( ) ,
] ) ;
}
}
2026-02-17 15:03:34 +00:00
// Special handling: Tailscale needs host network; mempool stack needs archy-net
2026-02-01 18:46:35 +00:00
let is_tailscale = package_id = = " tailscale " ;
2026-02-17 15:03:34 +00:00
let needs_archy_net = matches! (
package_id ,
2026-02-25 18:04:41 +00:00
" bitcoin-knots " | " bitcoin " | " bitcoin-core "
2026-03-14 02:45:28 +00:00
| " lnd "
2026-03-08 02:16:02 +00:00
| " mempool " | " mempool-web " | " mempool-api " | " mempool-electrs " | " electrs " | " mysql-mempool " | " archy-mempool-db " | " archy-mempool-web "
2026-03-14 02:45:28 +00:00
| " btcpay-server " | " btcpayserver " | " archy-btcpay-db " | " archy-nbxplorer " | " nbxplorer "
2026-03-09 07:43:12 +00:00
| " fedimint " | " fedimint-gateway "
2026-02-17 15:03:34 +00:00
) ;
2026-02-01 18:46:35 +00:00
if is_tailscale {
run_args . push ( " --network=host " ) ;
run_args . push ( " --privileged " ) ;
run_args . push ( " --cap-add=NET_ADMIN " ) ;
run_args . push ( " --cap-add=NET_RAW " ) ;
run_args . push ( " --device=/dev/net/tun " ) ;
2026-02-17 15:03:34 +00:00
} else if needs_archy_net {
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " network " , " create " , " archy-net " ] )
. output ( )
. await ;
run_args . push ( " --network=archy-net " ) ;
2026-02-01 18:46:35 +00:00
}
2026-03-04 05:23:42 +00:00
2026-03-05 08:24:56 +00:00
// Security hardening (skip for privileged containers like Tailscale)
let security_caps : Vec < String > = if ! is_tailscale {
get_app_capabilities ( package_id )
} else {
vec! [ ]
} ;
let readonly_compatible = ! is_tailscale & & is_readonly_compatible ( package_id ) ;
if ! is_tailscale {
run_args . push ( " --cap-drop=ALL " ) ;
run_args . push ( " --security-opt=no-new-privileges:true " ) ;
for cap in & security_caps {
run_args . push ( cap ) ;
}
if readonly_compatible {
run_args . push ( " --read-only " ) ;
run_args . push ( " --tmpfs=/tmp:rw,noexec,nosuid,size=256m " ) ;
run_args . push ( " --tmpfs=/run:rw,noexec,nosuid,size=64m " ) ;
}
}
2026-02-01 18:46:35 +00:00
// Create data directories if they don't exist
for volume in & volumes {
if let Some ( host_path ) = volume . split ( ':' ) . next ( ) {
if host_path . starts_with ( " /var/lib/archipelago/ " ) {
debug! ( " Creating directory: {} " , host_path ) ;
let create_dir = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " mkdir " , " -p " , host_path ] )
. output ( )
. await ;
2026-03-04 05:23:42 +00:00
2026-02-01 18:46:35 +00:00
if let Err ( e ) = create_dir {
debug! ( " Failed to create directory {}: {} " , host_path , e ) ;
}
2026-02-25 18:04:41 +00:00
// Grafana runs as UID 472 - fix permissions so it can write
if package_id = = " grafana " & & host_path . contains ( " grafana " ) {
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " chown " , " -R " , " 472:472 " , host_path ] )
. output ( )
. await ;
}
2026-02-01 18:46:35 +00:00
}
}
}
2026-03-04 05:23:42 +00:00
2026-03-08 02:16:02 +00:00
// Pre-install: Create bitcoin.conf for Bitcoin nodes with RPC + txindex
if matches! ( package_id , " bitcoin " | " bitcoin-core " | " bitcoin-knots " ) {
let bitcoin_dir = " /var/lib/archipelago/bitcoin " ;
let conf_path = format! ( " {} /bitcoin.conf " , bitcoin_dir ) ;
let bitcoin_conf = " \
server = 1 \ n \
txindex = 1 \ n \
rpcuser = archipelago \ n \
rpcpassword = archipelago123 \ n \
rpcbind = 0. 0. 0.0 \ n \
rpcallowip = 0. 0. 0.0 / 0 \ n \
rpcport = 8332 \ n \
listen = 1 \ n \
printtoconsole = 1 \ n " ;
2026-03-11 18:09:16 +00:00
let _ = tokio ::fs ::create_dir_all ( bitcoin_dir ) . await ;
let _ = tokio ::fs ::write ( & conf_path , bitcoin_conf ) . await ;
2026-03-08 02:16:02 +00:00
info! ( " Created bitcoin.conf at {} with RPC + txindex enabled " , conf_path ) ;
}
2026-02-01 18:46:35 +00:00
// Add port mappings (skip if host network mode like Tailscale)
if ! is_tailscale {
for port in & ports {
run_args . push ( " -p " ) ;
run_args . push ( port ) ;
}
}
2026-03-04 05:23:42 +00:00
2026-02-01 18:46:35 +00:00
// Add volume mounts
for volume in & volumes {
run_args . push ( " -v " ) ;
run_args . push ( volume ) ;
}
2026-03-04 05:23:42 +00:00
2026-02-01 18:46:35 +00:00
// Add environment variables
for env in & env_vars {
run_args . push ( " -e " ) ;
run_args . push ( env ) ;
}
2026-03-04 05:23:42 +00:00
2026-03-14 02:45:28 +00:00
// Resource limits: per-app memory and CPU
let memory_limit = get_memory_limit ( package_id ) ;
2026-02-25 18:04:41 +00:00
let mem_arg = format! ( " --memory= {} " , memory_limit ) ;
run_args . push ( & mem_arg ) ;
run_args . push ( " --cpus=2 " ) ;
2026-03-04 05:23:42 +00:00
2026-03-14 02:45:28 +00:00
// Health check definitions
let health_args = get_health_check_args ( package_id ) ;
for arg in & health_args {
run_args . push ( arg ) ;
}
2026-02-01 18:46:35 +00:00
// Finally, the image
run_args . push ( docker_image ) ;
debug! ( " Running container with args: {:?} " , run_args ) ;
2026-03-04 05:23:42 +00:00
2026-02-01 18:46:35 +00:00
// Build command with optional custom command
let mut cmd = tokio ::process ::Command ::new ( " sudo " ) ;
cmd . args ( & run_args ) ;
2026-03-04 05:23:42 +00:00
// Add custom command/args if specified
2026-02-01 18:46:35 +00:00
if let Some ( custom_cmd ) = custom_command {
cmd . arg ( custom_cmd ) ;
2026-02-17 15:03:34 +00:00
} else if let Some ( args ) = custom_args {
cmd . args ( args ) ;
2026-02-01 18:46:35 +00:00
}
2026-03-04 05:23:42 +00:00
2026-02-01 18:46:35 +00:00
let run_output = cmd
. output ( )
. await
. context ( " Failed to run container " ) ? ;
if ! run_output . status . success ( ) {
let stderr = String ::from_utf8_lossy ( & run_output . stderr ) ;
return Err ( anyhow ::anyhow! ( " Failed to start container: {} " , stderr ) ) ;
}
let container_id = String ::from_utf8_lossy ( & run_output . stdout ) . trim ( ) . to_string ( ) ;
2026-03-04 05:23:42 +00:00
feat: cloud native file browser, settings Claude auth, deploy hardening
- Add native Cloud file browser with FileBrowser API integration
- Add cloud store, filebrowser-client, useAudioPlayer, useFileType composables
- Add Cloud components: FileGrid, FileCard, FileCardGrid, CloudToolbar
- Add Claude authentication section to Settings with OAuth status check
- Harden deploy script to preserve /aiui/ and claude-login.html
- Add nginx proxies for btcpay, homeassistant, filebrowser (HTTPS block)
- Add app configs for filebrowser, searxng, penpot in package.rs
- Update goal progress tracking with app aliases
- Improve mobile back button composable with ResizeObserver
- Update various views with cloud integration and UI refinements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:05:01 +00:00
// Post-install: Nextcloud needs trusted domains configured for iframe embedding
if package_id = = " nextcloud " {
let host_ip = self . config . host_ip . clone ( ) ;
tokio ::spawn ( async move {
// Wait for Nextcloud to finish first-run initialization
tokio ::time ::sleep ( std ::time ::Duration ::from_secs ( 30 ) ) . await ;
for domain_idx in 1 ..= 2 u8 {
let value = if domain_idx = = 1 { host_ip . as_str ( ) } else { " localhost " } ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [
" podman " , " exec " , " -u " , " 33 " , " nextcloud " ,
" php " , " occ " , " config:system:set " ,
" trusted_domains " , & domain_idx . to_string ( ) ,
" --value " , value ,
] )
. output ( )
. await ;
}
info! ( " Nextcloud trusted domains configured for {} " , host_ip ) ;
} ) ;
}
2026-03-08 02:16:02 +00:00
// Post-install: Start electrs-ui container for electrs
if matches! ( package_id , " mempool-electrs " | " electrs " ) {
tokio ::spawn ( async move {
// Build and start electrs-ui with host networking so it can reach backend on 127.0.0.1:5678
let ui_dir = " /opt/archipelago/docker/electrs-ui " ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " build " , " -t " , " localhost/electrs-ui " , ui_dir ] )
. output ( )
. await ;
// Remove old UI container if it exists
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " rm " , " -f " , " electrs-ui " ] )
. output ( )
. await ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [
" podman " , " run " , " -d " , " --name " , " electrs-ui " ,
" --restart=unless-stopped " , " --network=host " ,
" localhost/electrs-ui " ,
] )
. output ( )
. await ;
info! ( " Electrs UI container started on port 50002 " ) ;
} ) ;
}
2026-02-01 18:46:35 +00:00
Ok ( serde_json ::json! ( {
" success " : true ,
" package_id " : package_id ,
" container_id " : container_id ,
" message " : format ! ( " Package {} installed and started " , package_id )
} ) )
}
2026-02-25 18:04:41 +00:00
/// Install Immich stack (postgres + redis + server)
async fn install_immich_stack ( & self ) -> Result < serde_json ::Value > {
let check = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " ps " , " -a " , " --format " , " {{.Names}} " ] )
. output ( )
. await
. context ( " Failed to list containers " ) ? ;
let stdout = String ::from_utf8_lossy ( & check . stdout ) ;
if stdout . contains ( " immich_server " ) {
return Err ( anyhow ::anyhow! ( " Immich already installed. Stop and remove it first. " ) ) ;
}
2026-02-25 18:20:50 +00:00
if stdout . contains ( " immich \n " ) | | stdout . lines ( ) . any ( | l | l . trim ( ) = = " immich " ) {
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " stop " , " immich " ] )
. output ( )
. await ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " rm " , " -f " , " immich " ] )
. output ( )
. await ;
}
2026-02-25 18:04:41 +00:00
let images = [
" ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 " ,
" docker.io/valkey/valkey:7-alpine " ,
" ghcr.io/immich-app/immich-server:release " ,
] ;
for img in & images {
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " pull " , img ] )
. output ( )
. await ;
}
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " mkdir " , " -p " , " /var/lib/archipelago/immich " , " /var/lib/archipelago/immich-db " ] )
. output ( )
. await ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " network " , " create " , " immich-net " ] )
. output ( )
. await ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [
" podman " , " run " , " -d " , " --name " , " immich_postgres " , " --restart " , " unless-stopped " ,
" --network " , " immich-net " ,
" -v " , " /var/lib/archipelago/immich-db:/var/lib/postgresql/data " ,
" -e " , " POSTGRES_PASSWORD=immichpass " , " -e " , " POSTGRES_USER=postgres " , " -e " , " POSTGRES_DB=immich " ,
" ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 " ,
] )
. output ( )
. await ;
tokio ::time ::sleep ( std ::time ::Duration ::from_secs ( 5 ) ) . await ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [
" podman " , " run " , " -d " , " --name " , " immich_redis " , " --restart " , " unless-stopped " ,
" --network " , " immich-net " ,
" docker.io/valkey/valkey:7-alpine " ,
] )
. output ( )
. await ;
tokio ::time ::sleep ( std ::time ::Duration ::from_secs ( 2 ) ) . await ;
let run = tokio ::process ::Command ::new ( " sudo " )
. args ( [
" podman " , " run " , " -d " , " --name " , " immich_server " , " --restart " , " unless-stopped " ,
" --network " , " immich-net " , " -p " , " 2283:2283 " ,
" -v " , " /var/lib/archipelago/immich:/usr/src/app/upload " ,
" -e " , " DB_HOSTNAME=immich_postgres " , " -e " , " DB_USERNAME=postgres " ,
" -e " , " DB_PASSWORD=immichpass " , " -e " , " DB_DATABASE_NAME=immich " ,
" -e " , " REDIS_HOSTNAME=immich_redis " , " -e " , " UPLOAD_LOCATION=/usr/src/app/upload " ,
" ghcr.io/immich-app/immich-server:release " ,
] )
. output ( )
. await
. context ( " Failed to start immich_server " ) ? ;
if ! run . status . success ( ) {
let stderr = String ::from_utf8_lossy ( & run . stderr ) ;
return Err ( anyhow ::anyhow! ( " Failed to start Immich server: {} " , stderr ) ) ;
}
Ok ( serde_json ::json! ( {
" success " : true ,
" package_id " : " immich " ,
" message " : " Immich stack installed and started "
} ) )
}
/// Install Penpot stack (postgres + valkey + backend + exporter + frontend)
async fn install_penpot_stack ( & self ) -> Result < serde_json ::Value > {
let check = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " ps " , " -a " , " --format " , " {{.Names}} " ] )
. output ( )
. await
. context ( " Failed to list containers " ) ? ;
let stdout = String ::from_utf8_lossy ( & check . stdout ) ;
if stdout . contains ( " penpot-frontend " ) {
return Err ( anyhow ::anyhow! ( " Penpot already installed. Stop and remove it first. " ) ) ;
}
let images = [
" docker.io/postgres:15 " ,
" docker.io/valkey/valkey:8.1 " ,
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
" docker.io/penpotapp/backend:2.4 " ,
" docker.io/penpotapp/exporter:2.4 " ,
" docker.io/penpotapp/frontend:2.4 " ,
2026-02-25 18:04:41 +00:00
] ;
for img in & images {
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " pull " , img ] )
. output ( )
. await ;
}
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " mkdir " , " -p " , " /var/lib/archipelago/penpot-assets " ] )
. output ( )
. await ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " network " , " create " , " penpot-net " ] )
. output ( )
. await ;
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
// Generate a stable secret key derived from the data directory
let secret = {
use sha2 ::{ Digest , Sha256 } ;
let mut hasher = Sha256 ::new ( ) ;
hasher . update ( b " penpot-secret- " ) ;
hasher . update ( self . config . data_dir . to_string_lossy ( ) . as_bytes ( ) ) ;
hex ::encode ( hasher . finalize ( ) )
} ;
2026-02-25 18:04:41 +00:00
let host_ip = & self . config . host_ip ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [
" podman " , " run " , " -d " , " --name " , " penpot-postgres " , " --restart " , " unless-stopped " ,
" --network " , " penpot-net " ,
" -v " , " /var/lib/archipelago/penpot-postgres:/var/lib/postgresql/data " ,
" -e " , " POSTGRES_DB=penpot " , " -e " , " POSTGRES_USER=penpot " , " -e " , " POSTGRES_PASSWORD=penpot " ,
" docker.io/postgres:15 " ,
] )
. output ( )
. await ;
tokio ::time ::sleep ( std ::time ::Duration ::from_secs ( 5 ) ) . await ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [
" podman " , " run " , " -d " , " --name " , " penpot-valkey " , " --restart " , " unless-stopped " ,
" --network " , " penpot-net " ,
" -e " , " VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu " ,
" docker.io/valkey/valkey:8.1 " ,
] )
. output ( )
. await ;
tokio ::time ::sleep ( std ::time ::Duration ::from_secs ( 3 ) ) . await ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [
" podman " , " run " , " -d " , " --name " , " penpot-backend " , " --restart " , " unless-stopped " ,
" --network " , " penpot-net " ,
" -v " , " /var/lib/archipelago/penpot-assets:/opt/data/assets " ,
" -e " , & format! ( " PENPOT_PUBLIC_URI=http:// {} :9001 " , host_ip ) ,
" -e " , & format! ( " PENPOT_SECRET_KEY= {} " , secret ) ,
" -e " , " PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot " ,
" -e " , " PENPOT_DATABASE_USERNAME=penpot " , " -e " , " PENPOT_DATABASE_PASSWORD=penpot " ,
" -e " , " PENPOT_REDIS_URI=redis://penpot-valkey/0 " ,
" -e " , " PENPOT_OBJECTS_STORAGE_BACKEND=fs " ,
" -e " , " PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets " ,
" -e " , " PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies " ,
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
" docker.io/penpotapp/backend:2.4 " ,
2026-02-25 18:04:41 +00:00
] )
. output ( )
. await ;
tokio ::time ::sleep ( std ::time ::Duration ::from_secs ( 5 ) ) . await ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [
" podman " , " run " , " -d " , " --name " , " penpot-exporter " , " --restart " , " unless-stopped " ,
" --network " , " penpot-net " ,
" -e " , & format! ( " PENPOT_SECRET_KEY= {} " , secret ) ,
" -e " , " PENPOT_PUBLIC_URI=http://penpot-frontend:8080 " ,
" -e " , " PENPOT_REDIS_URI=redis://penpot-valkey/0 " ,
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
" docker.io/penpotapp/exporter:2.4 " ,
2026-02-25 18:04:41 +00:00
] )
. output ( )
. await ;
tokio ::time ::sleep ( std ::time ::Duration ::from_secs ( 2 ) ) . await ;
let run = tokio ::process ::Command ::new ( " sudo " )
. args ( [
" podman " , " run " , " -d " , " --name " , " penpot-frontend " , " --restart " , " unless-stopped " ,
" --network " , " penpot-net " , " -p " , " 9001:8080 " ,
" -v " , " /var/lib/archipelago/penpot-assets:/opt/data/assets " ,
" -e " , & format! ( " PENPOT_PUBLIC_URI=http:// {} :9001 " , host_ip ) ,
" -e " , " PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies " ,
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
" docker.io/penpotapp/frontend:2.4 " ,
2026-02-25 18:04:41 +00:00
] )
. output ( )
. await
. context ( " Failed to start penpot-frontend " ) ? ;
if ! run . status . success ( ) {
let stderr = String ::from_utf8_lossy ( & run . stderr ) ;
return Err ( anyhow ::anyhow! ( " Failed to start Penpot frontend: {} " , stderr ) ) ;
}
Ok ( serde_json ::json! ( {
" success " : true ,
" package_id " : " penpot " ,
" message " : " Penpot stack installed and started "
} ) )
}
2026-03-04 05:23:42 +00:00
pub ( super ) async fn handle_package_start (
2026-01-27 23:21:26 +00:00
& self ,
params : Option < serde_json ::Value > ,
) -> Result < serde_json ::Value > {
let params = params . ok_or_else ( | | anyhow ::anyhow! ( " Missing params " ) ) ? ;
let package_id = params
. get ( " id " )
. and_then ( | v | v . as_str ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " Missing package id " ) ) ? ;
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
validate_app_id ( package_id ) ? ;
2026-01-27 23:21:26 +00:00
2026-02-17 15:03:34 +00:00
let containers = get_containers_for_app ( package_id ) . await ? ;
let to_start : Vec < String > = if containers . is_empty ( ) {
vec! [ format! ( " archy- {} " , package_id ) ]
2026-02-01 13:24:03 +00:00
} else {
2026-02-25 18:04:41 +00:00
let order : & [ & str ] = match package_id {
" mempool " | " mempool-web " = > & [ " archy-mempool-db " , " mysql-mempool " , " mempool-electrs " , " mempool-api " , " archy-mempool-api " , " archy-mempool-web " , " mempool " ] ,
" immich " = > & [ " immich_postgres " , " immich_redis " , " immich_server " ] ,
" penpot " | " penpot-frontend " = > & [ " penpot-postgres " , " penpot-valkey " , " penpot-backend " , " penpot-exporter " , " penpot-frontend " ] ,
_ = > & [ " archy-mempool-db " , " mysql-mempool " , " mempool-electrs " , " mempool-api " , " archy-mempool-api " , " archy-mempool-web " , " mempool " ] ,
} ;
2026-02-17 15:03:34 +00:00
let mut sorted = containers ;
sorted . sort_by_key ( | c | order . iter ( ) . position ( | o | * o = = c ) . unwrap_or ( 99 ) ) ;
sorted
2026-02-01 13:24:03 +00:00
} ;
2026-01-27 23:21:26 +00:00
2026-02-17 15:03:34 +00:00
for name in to_start {
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " start " , & name ] )
. output ( )
. await ;
2026-01-27 23:21:26 +00:00
}
Ok ( serde_json ::Value ::Null )
}
2026-03-04 05:23:42 +00:00
pub ( super ) async fn handle_package_stop (
2026-01-27 23:21:26 +00:00
& self ,
params : Option < serde_json ::Value > ,
) -> Result < serde_json ::Value > {
let params = params . ok_or_else ( | | anyhow ::anyhow! ( " Missing params " ) ) ? ;
let package_id = params
. get ( " id " )
. and_then ( | v | v . as_str ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " Missing package id " ) ) ? ;
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
validate_app_id ( package_id ) ? ;
2026-01-27 23:21:26 +00:00
2026-02-17 15:03:34 +00:00
let containers = get_containers_for_app ( package_id ) . await ? ;
if containers . is_empty ( ) {
let container_name = format! ( " archy- {} " , package_id ) ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " stop " , & container_name ] )
. output ( )
. await ;
return Ok ( serde_json ::Value ::Null ) ;
}
2026-01-27 23:21:26 +00:00
2026-02-17 15:03:34 +00:00
for name in containers {
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " stop " , & name ] )
. output ( )
. await ;
2026-01-27 23:21:26 +00:00
}
Ok ( serde_json ::Value ::Null )
}
2026-03-04 05:23:42 +00:00
pub ( super ) async fn handle_package_restart (
2026-01-27 23:21:26 +00:00
& self ,
params : Option < serde_json ::Value > ,
) -> Result < serde_json ::Value > {
let params = params . ok_or_else ( | | anyhow ::anyhow! ( " Missing params " ) ) ? ;
let package_id = params
. get ( " id " )
. and_then ( | v | v . as_str ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " Missing package id " ) ) ? ;
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
validate_app_id ( package_id ) ? ;
2026-01-27 23:21:26 +00:00
2026-02-17 15:03:34 +00:00
let containers = get_containers_for_app ( package_id ) . await ? ;
if containers . is_empty ( ) {
let container_name = format! ( " archy- {} " , package_id ) ;
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " restart " , & container_name ] )
. output ( )
. await ;
return Ok ( serde_json ::Value ::Null ) ;
}
2026-01-27 23:21:26 +00:00
2026-02-17 15:03:34 +00:00
for name in containers {
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " restart " , & name ] )
. output ( )
. await ;
2026-01-27 23:21:26 +00:00
}
Ok ( serde_json ::Value ::Null )
}
2026-02-01 06:04:36 +00:00
2026-03-04 05:23:42 +00:00
/// Uninstall a package: stop and remove all related containers, clean data.
pub ( super ) async fn handle_package_uninstall (
2026-02-17 15:03:34 +00:00
& self ,
params : Option < serde_json ::Value > ,
) -> Result < serde_json ::Value > {
let params = params . ok_or_else ( | | anyhow ::anyhow! ( " Missing params " ) ) ? ;
let package_id = params
. get ( " id " )
. and_then ( | v | v . as_str ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " Missing package id " ) ) ? ;
2026-03-06 03:26:56 +00:00
validate_app_id ( package_id ) ? ;
2026-02-17 15:03:34 +00:00
let preserve_data = params
. get ( " preserve_data " )
. and_then ( | v | v . as_bool ( ) )
. unwrap_or ( false ) ;
let containers_to_remove = get_containers_for_app ( package_id ) . await ? ;
2026-03-12 00:19:30 +00:00
if containers_to_remove . is_empty ( ) {
tracing ::warn! ( " Uninstall {}: no containers found " , package_id ) ;
}
let mut stopped = 0 u32 ;
let mut removed = 0 u32 ;
let mut errors = Vec ::new ( ) ;
2026-02-17 15:03:34 +00:00
for name in & containers_to_remove {
2026-03-12 00:19:30 +00:00
tracing ::info! ( " Uninstall {}: stopping container {} " , package_id , name ) ;
let stop_out = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " stop " , " -t " , " 10 " , name ] )
2026-02-17 15:03:34 +00:00
. output ( )
. await ;
2026-03-12 00:19:30 +00:00
match stop_out {
Ok ( o ) if o . status . success ( ) = > stopped + = 1 ,
Ok ( o ) = > {
let stderr = String ::from_utf8_lossy ( & o . stderr ) ;
tracing ::warn! ( " Uninstall {}: stop {} failed: {} " , package_id , name , stderr . trim ( ) ) ;
}
Err ( e ) = > {
tracing ::warn! ( " Uninstall {}: stop {} error: {} " , package_id , name , e ) ;
}
}
tracing ::info! ( " Uninstall {}: removing container {} " , package_id , name ) ;
let rm_out = tokio ::process ::Command ::new ( " sudo " )
2026-02-17 15:03:34 +00:00
. args ( [ " podman " , " rm " , " -f " , name ] )
. output ( )
. await ;
2026-03-12 00:19:30 +00:00
match rm_out {
Ok ( o ) if o . status . success ( ) = > removed + = 1 ,
Ok ( o ) = > {
let stderr = String ::from_utf8_lossy ( & o . stderr ) ;
let msg = format! ( " Failed to remove {} : {} " , name , stderr . trim ( ) ) ;
tracing ::error! ( " Uninstall {}: {} " , package_id , msg ) ;
errors . push ( msg ) ;
}
Err ( e ) = > {
let msg = format! ( " Failed to remove {} : {} " , name , e ) ;
tracing ::error! ( " Uninstall {}: {} " , package_id , msg ) ;
errors . push ( msg ) ;
}
}
2026-02-17 15:03:34 +00:00
}
// Release port allocation
if let Ok ( mut allocator ) = self . port_allocator . lock ( ) {
let _ = allocator . release ( package_id ) ;
}
// Clean data directories unless preserve_data
if ! preserve_data {
let data_dirs = get_data_dirs_for_app ( package_id ) ;
for dir in & data_dirs {
2026-03-12 00:19:30 +00:00
tracing ::info! ( " Uninstall {}: removing data {} " , package_id , dir ) ;
let rm_out = tokio ::process ::Command ::new ( " sudo " )
2026-02-17 15:03:34 +00:00
. args ( [ " rm " , " -rf " , dir ] )
. output ( )
. await ;
2026-03-12 00:19:30 +00:00
if let Ok ( o ) = rm_out {
if ! o . status . success ( ) {
tracing ::warn! ( " Uninstall {}: rm {} failed " , package_id , dir ) ;
}
}
2026-02-17 15:03:34 +00:00
}
}
2026-03-12 00:19:30 +00:00
if ! errors . is_empty ( ) {
tracing ::error! ( " Uninstall {} completed with errors: {:?} " , package_id , errors ) ;
} else {
tracing ::info! ( " Uninstall {} complete: stopped={}, removed={} " , package_id , stopped , removed ) ;
}
Ok ( serde_json ::json! ( {
" status " : if errors . is_empty ( ) { " uninstalled " } else { " partial " } ,
" stopped " : stopped ,
" removed " : removed ,
" errors " : errors ,
} ) )
2026-02-17 15:03:34 +00:00
}
2026-02-01 06:04:36 +00:00
/// Start a bundled app (create container from pre-loaded image if needed, then start)
2026-03-04 05:23:42 +00:00
pub ( super ) async fn handle_bundled_app_start (
2026-02-01 06:04:36 +00:00
& self ,
params : Option < serde_json ::Value > ,
) -> Result < serde_json ::Value > {
let params = params . ok_or_else ( | | anyhow ::anyhow! ( " Missing params " ) ) ? ;
let app_id = params
. get ( " app_id " )
. and_then ( | v | v . as_str ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " Missing app_id " ) ) ? ;
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
validate_app_id ( app_id ) ? ;
2026-02-01 06:04:36 +00:00
let image = params
. get ( " image " )
. and_then ( | v | v . as_str ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " Missing image " ) ) ? ;
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
if ! is_valid_docker_image ( image ) {
return Err ( anyhow ::anyhow! ( " Invalid Docker image format " ) ) ;
}
2026-02-01 06:04:36 +00:00
let ports = params
. get ( " ports " )
. and_then ( | v | v . as_array ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " Missing ports " ) ) ? ;
let volumes = params
. get ( " volumes " )
. and_then ( | v | v . as_array ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " Missing volumes " ) ) ? ;
let check_output = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " ps " , " -a " , " --format " , " {{.Names}} " , " --filter " , & format! ( " name= {} " , app_id ) ] )
. output ( )
. await
. context ( " Failed to check container " ) ? ;
let existing = String ::from_utf8_lossy ( & check_output . stdout ) ;
2026-03-04 05:23:42 +00:00
2026-02-01 06:04:36 +00:00
if existing . trim ( ) . is_empty ( ) {
let mut cmd = tokio ::process ::Command ::new ( " sudo " ) ;
cmd . args ( [ " podman " , " run " , " -d " , " --name " , app_id ] ) ;
for port in ports {
if let ( Some ( host ) , Some ( container ) ) = (
port . get ( " host " ) . and_then ( | v | v . as_u64 ( ) ) ,
port . get ( " container " ) . and_then ( | v | v . as_u64 ( ) ) ,
) {
cmd . arg ( " -p " ) . arg ( format! ( " {} : {} " , host , container ) ) ;
}
}
for volume in volumes {
if let ( Some ( host ) , Some ( container ) ) = (
volume . get ( " host " ) . and_then ( | v | v . as_str ( ) ) ,
volume . get ( " container " ) . and_then ( | v | v . as_str ( ) ) ,
) {
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
// Validate host path: must be under /var/lib/archipelago/ and no traversal
if ! host . starts_with ( " /var/lib/archipelago/ " ) | | host . contains ( " .. " ) | | host . contains ( '\0' ) {
return Err ( anyhow ::anyhow! (
" Volume host path must be under /var/lib/archipelago/ and cannot contain path traversal "
) ) ;
}
// Validate container path
if container . contains ( " .. " ) | | container . contains ( '\0' ) {
return Err ( anyhow ::anyhow! ( " Invalid container mount path " ) ) ;
}
2026-02-01 06:04:36 +00:00
let _ = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " mkdir " , " -p " , host ] )
. output ( )
. await ;
cmd . arg ( " -v " ) . arg ( format! ( " {} : {} " , host , container ) ) ;
}
}
cmd . arg ( image ) ;
let output = cmd . output ( ) . await . context ( " Failed to create container " ) ? ;
if ! output . status . success ( ) {
let stderr = String ::from_utf8_lossy ( & output . stderr ) ;
return Err ( anyhow ::anyhow! ( " Failed to create container: {} " , stderr ) ) ;
}
} else {
let output = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " start " , app_id ] )
. output ( )
. await
. context ( " Failed to start container " ) ? ;
if ! output . status . success ( ) {
let stderr = String ::from_utf8_lossy ( & output . stderr ) ;
return Err ( anyhow ::anyhow! ( " Failed to start container: {} " , stderr ) ) ;
}
}
Ok ( serde_json ::json! ( { " status " : " started " , " app_id " : app_id } ) )
}
/// Stop a bundled app
2026-03-04 05:23:42 +00:00
pub ( super ) async fn handle_bundled_app_stop (
2026-02-01 06:04:36 +00:00
& self ,
params : Option < serde_json ::Value > ,
) -> Result < serde_json ::Value > {
let params = params . ok_or_else ( | | anyhow ::anyhow! ( " Missing params " ) ) ? ;
let app_id = params
. get ( " app_id " )
. and_then ( | v | v . as_str ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " Missing app_id " ) ) ? ;
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
validate_app_id ( app_id ) ? ;
2026-02-01 06:04:36 +00:00
let output = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " stop " , app_id ] )
. output ( )
. await
. context ( " Failed to stop container " ) ? ;
if ! output . status . success ( ) {
let stderr = String ::from_utf8_lossy ( & output . stderr ) ;
return Err ( anyhow ::anyhow! ( " Failed to stop container: {} " , stderr ) ) ;
}
Ok ( serde_json ::json! ( { " status " : " stopped " , " app_id " : app_id } ) )
}
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
/// Set install progress for a package and broadcast the update.
/// Creates a minimal package entry if one doesn't exist yet.
async fn set_install_progress ( & self , package_id : & str , downloaded : u64 , size : u64 ) {
let ( mut data , _rev ) = self . state_manager . get_snapshot ( ) . await ;
let entry = data
. package_data
. entry ( package_id . to_string ( ) )
. or_insert_with ( | | create_installing_entry ( package_id ) ) ;
entry . state = PackageState ::Installing ;
entry . install_progress = Some ( InstallProgress { size , downloaded } ) ;
self . state_manager . update_data ( data ) . await ;
}
/// Clear install progress after pull completes or fails
async fn clear_install_progress ( & self , package_id : & str ) {
let ( mut data , _rev ) = self . state_manager . get_snapshot ( ) . await ;
if let Some ( entry ) = data . package_data . get_mut ( package_id ) {
entry . install_progress = None ;
}
self . state_manager . update_data ( data ) . await ;
}
/// Update install progress (static method for use in async closures)
async fn update_install_progress (
state_manager : & crate ::state ::StateManager ,
package_id : & str ,
downloaded : u64 ,
total : u64 ,
) {
let ( mut data , _rev ) = state_manager . get_snapshot ( ) . await ;
let entry = data
. package_data
. entry ( package_id . to_string ( ) )
. or_insert_with ( | | create_installing_entry ( package_id ) ) ;
entry . install_progress = Some ( InstallProgress {
size : total ,
downloaded ,
} ) ;
state_manager . update_data ( data ) . await ;
}
}
/// Create a minimal PackageDataEntry for a package being installed
fn create_installing_entry ( package_id : & str ) -> PackageDataEntry {
PackageDataEntry {
state : PackageState ::Installing ,
static_files : StaticFiles {
license : String ::new ( ) ,
instructions : String ::new ( ) ,
icon : format ! ( " /assets/img/app-icons/{}.png " , package_id ) ,
} ,
manifest : Manifest {
id : package_id . to_string ( ) ,
title : package_id . to_string ( ) ,
version : String ::new ( ) ,
description : Description {
short : " Installing... " . to_string ( ) ,
long : String ::new ( ) ,
} ,
release_notes : String ::new ( ) ,
license : String ::new ( ) ,
wrapper_repo : String ::new ( ) ,
upstream_repo : String ::new ( ) ,
support_site : String ::new ( ) ,
marketing_site : String ::new ( ) ,
donation_url : None ,
author : None ,
website : None ,
interfaces : None ,
feat: add app tier system — core/recommended/optional (SCALE-02, SCALE-03)
get_app_tier() classifies all apps:
- core: Bitcoin, LND, Electrs, Mempool, BTCPay, DWN, FileBrowser
- recommended: Fedimint, Grafana, Vaultwarden, Kuma, SearXNG, etc.
- optional: everything else
Tier field added to Manifest struct (data_model.rs) and exposed
via WebSocket package data for frontend tier badges.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 03:27:51 +00:00
tier : None ,
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
} ,
installed : None ,
install_progress : None ,
}
}
/// Parse podman pull progress output.
/// Podman outputs lines like: "Copying blob sha256:abc done | 50.0MiB / 100.0MiB"
/// Returns (downloaded_bytes, total_bytes) if parseable.
fn parse_pull_progress ( line : & str ) -> Option < ( u64 , u64 ) > {
// Look for "X.YMiB / Z.WMiB" or "X.YGiB / Z.WGiB" patterns
let line = line . trim ( ) ;
// Find the pattern "NUMBER UNIT / NUMBER UNIT"
let parts : Vec < & str > = line . split ( '/' ) . collect ( ) ;
if parts . len ( ) ! = 2 {
return None ;
}
let downloaded = parse_size_value ( parts [ 0 ] . trim ( ) ) ? ;
let total = parse_size_value ( parts [ 1 ] . trim ( ) ) ? ;
if total > 0 {
Some ( ( downloaded , total ) )
} else {
None
}
}
/// Parse a size value like "50.0MiB", "1.2GiB", "500KiB" into bytes
fn parse_size_value ( s : & str ) -> Option < u64 > {
// Extract the last token which should be "NUMBER UNIT" or "NUMBERUnit"
let s = s . trim ( ) ;
// Try to find the numeric part at the end of the string
// Podman formats: "50.0MiB", "1.2 GiB", etc.
let ( num_str , multiplier ) = if let Some ( pos ) = s . rfind ( " GiB " ) {
( s [ .. pos ] . trim ( ) . split_whitespace ( ) . last ( ) ? , 1024 * 1024 * 1024 )
} else if let Some ( pos ) = s . rfind ( " MiB " ) {
( s [ .. pos ] . trim ( ) . split_whitespace ( ) . last ( ) ? , 1024 * 1024 )
} else if let Some ( pos ) = s . rfind ( " KiB " ) {
( s [ .. pos ] . trim ( ) . split_whitespace ( ) . last ( ) ? , 1024 )
} else if let Some ( pos ) = s . rfind ( " GB " ) {
( s [ .. pos ] . trim ( ) . split_whitespace ( ) . last ( ) ? , 1_000_000_000 )
} else if let Some ( pos ) = s . rfind ( " MB " ) {
( s [ .. pos ] . trim ( ) . split_whitespace ( ) . last ( ) ? , 1_000_000 )
} else if let Some ( pos ) = s . rfind ( " KB " ) {
( s [ .. pos ] . trim ( ) . split_whitespace ( ) . last ( ) ? , 1_000 )
} else if let Some ( pos ) = s . rfind ( 'B' ) {
( s [ .. pos ] . trim ( ) . split_whitespace ( ) . last ( ) ? , 1 )
} else {
return None ;
} ;
let num : f64 = num_str . parse ( ) . ok ( ) ? ;
Some ( ( num * multiplier as f64 ) as u64 )
2026-02-17 15:03:34 +00:00
}
/// Get all container names for an app (handles multi-container apps like mempool)
async fn get_containers_for_app ( package_id : & str ) -> Result < Vec < String > > {
2026-03-06 03:26:56 +00:00
validate_app_id ( package_id ) ? ;
2026-02-17 15:03:34 +00:00
let output = tokio ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " ps " , " -a " , " --format " , " {{.Names}} " ] )
. output ( )
. await
. context ( " Failed to list containers " ) ? ;
let stdout = String ::from_utf8_lossy ( & output . stdout ) ;
let all : Vec < & str > = stdout . lines ( ) . filter ( | s | ! s . is_empty ( ) ) . collect ( ) ;
let patterns : Vec < String > = match package_id {
" mempool " | " mempool-web " = > {
vec! [
" mempool-electrs " . into ( ) ,
" mempool-api " . into ( ) ,
" archy-mempool-api " . into ( ) ,
" archy-mempool-web " . into ( ) ,
" mempool " . into ( ) ,
" archy-mempool-db " . into ( ) ,
" mysql-mempool " . into ( ) ,
]
}
2026-03-09 07:43:12 +00:00
" fedimint " = > vec! [ " fedimint " . into ( ) , " fedimint-ui " . into ( ) , " archy-fedimint " . into ( ) , " fedimint-gateway " . into ( ) ] ,
" fedimint-gateway " = > vec! [ " fedimint-gateway " . into ( ) ] ,
2026-02-25 18:04:41 +00:00
" immich " = > vec! [
" immich_postgres " . into ( ) ,
" immich_redis " . into ( ) ,
" immich_server " . into ( ) ,
] ,
" penpot " | " penpot-frontend " = > vec! [
" penpot-postgres " . into ( ) ,
" penpot-valkey " . into ( ) ,
" penpot-backend " . into ( ) ,
" penpot-exporter " . into ( ) ,
" penpot-frontend " . into ( ) ,
] ,
2026-02-17 15:03:34 +00:00
_ = > vec! [ package_id . to_string ( ) , format! ( " archy- {} " , package_id ) ] ,
} ;
let mut result = Vec ::new ( ) ;
for name in all {
for pat in & patterns {
if name = = pat {
result . push ( name . to_string ( ) ) ;
break ;
}
}
}
Ok ( result )
}
2026-03-06 03:26:56 +00:00
/// Get data directories to clean for an app.
/// Caller must validate package_id before calling.
2026-02-17 15:03:34 +00:00
fn get_data_dirs_for_app ( package_id : & str ) -> Vec < String > {
let base = " /var/lib/archipelago " ;
match package_id {
" mempool " | " mempool-web " = > vec! [
format! ( " {} /mempool " , base ) ,
format! ( " {} /mysql-mempool " , base ) ,
format! ( " {} /mempool-electrs " , base ) ,
] ,
2026-03-09 07:43:12 +00:00
" fedimint " = > vec! [ format! ( " {} /fedimint " , base ) , format! ( " {} /fedimint-gateway " , base ) ] ,
" fedimint-gateway " = > vec! [ format! ( " {} /fedimint-gateway " , base ) ] ,
2026-02-25 18:04:41 +00:00
" immich " = > vec! [
format! ( " {} /immich " , base ) ,
format! ( " {} /immich-db " , base ) ,
] ,
" penpot " | " penpot-frontend " = > vec! [
format! ( " {} /penpot-assets " , base ) ,
format! ( " {} /penpot-postgres " , base ) ,
] ,
2026-02-17 15:03:34 +00:00
_ = > vec! [ format! ( " {} / {} " , base , package_id ) ] ,
}
2026-01-24 22:59:20 +00:00
}
2026-02-01 18:46:35 +00:00
2026-03-06 03:26:56 +00:00
/// Trusted Docker registries. Only images from these sources are allowed.
const TRUSTED_REGISTRIES : & [ & str ] = & [
" docker.io/ " ,
" ghcr.io/ " ,
" localhost/ " ,
] ;
/// Validate Docker image against trusted registry allowlist.
2026-03-08 02:16:02 +00:00
/// Detect which Bitcoin container is running on archy-net for DNS resolution.
/// Returns the container name to use as the RPC host (e.g., "bitcoin-knots").
fn detect_bitcoin_container_name ( ) -> String {
// Synchronous check — called from get_app_config which is sync
let output = std ::process ::Command ::new ( " sudo " )
. args ( [ " podman " , " ps " , " --format " , " {{.Names}} " ] )
. output ( ) ;
if let Ok ( out ) = output {
let names = String ::from_utf8_lossy ( & out . stdout ) ;
for candidate in & [ " bitcoin-knots " , " bitcoin-core " , " bitcoin " ] {
if names . lines ( ) . any ( | l | l . trim ( ) = = * candidate ) {
return candidate . to_string ( ) ;
}
}
}
// Default to bitcoin-knots (most common)
" bitcoin-knots " . to_string ( )
}
2026-02-01 18:46:35 +00:00
fn is_valid_docker_image ( image : & str ) -> bool {
2026-03-06 03:26:56 +00:00
if image . is_empty ( ) | | image . len ( ) > 256 {
return false ;
}
// Reject shell metacharacters
2026-02-01 18:46:35 +00:00
let dangerous_chars = [ '&' , '|' , ';' , '`' , '$' , '(' , ')' , '<' , '>' , '\n' , '\r' ] ;
if image . chars ( ) . any ( | c | dangerous_chars . contains ( & c ) ) {
return false ;
}
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
// Must come from a trusted registry — match the exact domain, not just prefix
let registry = match image . split ( '/' ) . next ( ) {
Some ( r ) = > r ,
None = > return false ,
} ;
matches! ( registry , " docker.io " | " ghcr.io " | " localhost " )
2026-03-06 03:26:56 +00:00
}
/// Validate that a package/app ID is safe (lowercase alphanumeric + hyphens, 1-64 chars).
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
pub ( super ) fn validate_app_id ( id : & str ) -> Result < ( ) > {
2026-03-06 03:26:56 +00:00
if id . is_empty ( ) | | id . len ( ) > 64 {
anyhow ::bail! ( " Invalid app id: must be 1-64 characters " ) ;
2026-02-01 18:46:35 +00:00
}
2026-03-06 03:26:56 +00:00
if ! id . bytes ( ) . all ( | b | b . is_ascii_lowercase ( ) | | b . is_ascii_digit ( ) | | b = = b '-' ) {
anyhow ::bail! ( " Invalid app id: only lowercase letters, digits, and hyphens allowed " ) ;
}
if id . starts_with ( '-' ) {
anyhow ::bail! ( " Invalid app id: must not start with a hyphen " ) ;
2026-02-01 18:46:35 +00:00
}
2026-03-06 03:26:56 +00:00
Ok ( ( ) )
2026-02-01 18:46:35 +00:00
}
2026-03-05 08:24:56 +00:00
/// Per-app Linux capabilities needed beyond the default cap-drop=ALL.
/// Most apps need CHOWN/SETUID/SETGID for internal user switching.
fn get_app_capabilities ( app_id : & str ) -> Vec < String > {
match app_id {
// Apps that need user switching and file ownership changes
" nextcloud " | " homeassistant " | " home-assistant " | " btcpay-server " | " btcpayserver "
| " jellyfin " | " onlyoffice " | " onlyoffice-documentserver " | " portainer " = > vec! [
" --cap-add=CHOWN " . to_string ( ) ,
" --cap-add=SETUID " . to_string ( ) ,
" --cap-add=SETGID " . to_string ( ) ,
" --cap-add=DAC_OVERRIDE " . to_string ( ) ,
] ,
// Nginx Proxy Manager needs to bind low ports
" nginx-proxy-manager " = > vec! [
" --cap-add=CHOWN " . to_string ( ) ,
" --cap-add=SETUID " . to_string ( ) ,
" --cap-add=SETGID " . to_string ( ) ,
" --cap-add=NET_BIND_SERVICE " . to_string ( ) ,
] ,
2026-03-12 22:19:04 +00:00
// Bitcoin and Lightning need file ownership ops + DAC_OVERRIDE for data dir access
2026-03-09 07:43:12 +00:00
" bitcoin " | " bitcoin-core " | " bitcoin-knots " | " lnd " | " fedimint " | " fedimint-gateway " = > vec! [
2026-03-12 22:19:04 +00:00
" --cap-add=CHOWN " . to_string ( ) ,
" --cap-add=FOWNER " . to_string ( ) ,
" --cap-add=SETUID " . to_string ( ) ,
" --cap-add=SETGID " . to_string ( ) ,
" --cap-add=DAC_OVERRIDE " . to_string ( ) ,
] ,
// Vaultwarden needs file ownership + NET_BIND_SERVICE (binds port 80 internally)
" vaultwarden " = > vec! [
" --cap-add=CHOWN " . to_string ( ) ,
" --cap-add=SETUID " . to_string ( ) ,
" --cap-add=SETGID " . to_string ( ) ,
" --cap-add=NET_BIND_SERVICE " . to_string ( ) ,
] ,
// PhotoPrism uses s6-overlay which needs privilege ops
" photoprism " = > vec! [
2026-03-05 08:24:56 +00:00
" --cap-add=CHOWN " . to_string ( ) ,
" --cap-add=SETUID " . to_string ( ) ,
" --cap-add=SETGID " . to_string ( ) ,
] ,
// Grafana runs as specific UID (472)
" grafana " = > vec! [
" --cap-add=CHOWN " . to_string ( ) ,
" --cap-add=SETUID " . to_string ( ) ,
" --cap-add=SETGID " . to_string ( ) ,
] ,
2026-03-12 22:19:04 +00:00
// Uptime-kuma startup script needs chown/fowner for /app/data ownership
" uptime-kuma " = > vec! [
" --cap-add=CHOWN " . to_string ( ) ,
" --cap-add=FOWNER " . to_string ( ) ,
" --cap-add=SETUID " . to_string ( ) ,
" --cap-add=SETGID " . to_string ( ) ,
] ,
// Minimal apps (searxng, filebrowser, etc.) need no extra caps
2026-03-05 08:24:56 +00:00
_ = > vec! [ ] ,
}
}
/// Apps safe to run with --read-only root filesystem.
/// These work correctly with volume mounts + tmpfs for /tmp and /run.
fn is_readonly_compatible ( app_id : & str ) -> bool {
matches! (
app_id ,
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
" searxng "
| " grafana "
| " filebrowser "
| " mempool-electrs "
| " electrs "
| " nostr-rs-relay "
| " ollama "
| " indeedhub "
2026-03-05 08:24:56 +00:00
)
}
2026-03-14 02:45:28 +00:00
/// Get container health check arguments for podman run.
/// Returns (health-cmd, interval, retries) args to append to run_args.
fn get_health_check_args ( app_id : & str ) -> Vec < String > {
let ( cmd , interval , retries ) = match app_id {
" bitcoin " | " bitcoin-core " | " bitcoin-knots " = > (
" bitcoin-cli -rpcuser=archipelago -rpcpassword=archipelago123 getblockchaininfo || exit 1 " ,
" 30s " , " 3 " ,
) ,
" lnd " = > (
" lncli getinfo || exit 1 " ,
" 30s " , " 3 " ,
) ,
" btcpay-server " | " btcpayserver " = > (
" curl -sf http://localhost:49392/ || exit 1 " ,
" 30s " , " 3 " ,
) ,
" mempool-api " = > (
" curl -sf http://localhost:8999/api/v1/backend-info || exit 1 " ,
" 30s " , " 3 " ,
) ,
" mempool " | " mempool-web " | " archy-mempool-web " = > (
" curl -sf http://localhost:8080/ || exit 1 " ,
" 30s " , " 3 " ,
) ,
" mempool-electrs " | " electrs " = > (
" curl -sf http://localhost:50001/ || exit 1 " ,
" 60s " , " 3 " ,
) ,
" nextcloud " = > (
" curl -sf http://localhost:80/status.php || exit 1 " ,
" 30s " , " 3 " ,
) ,
" homeassistant " | " home-assistant " = > (
" curl -sf http://localhost:8123/api/ || exit 1 " ,
" 30s " , " 3 " ,
) ,
" grafana " = > (
" curl -sf http://localhost:3000/api/health || exit 1 " ,
" 30s " , " 3 " ,
) ,
" jellyfin " = > (
" curl -sf http://localhost:8096/health || exit 1 " ,
" 30s " , " 3 " ,
) ,
" vaultwarden " = > (
" curl -sf http://localhost:80/alive || exit 1 " ,
" 30s " , " 3 " ,
) ,
" uptime-kuma " = > (
" curl -sf http://localhost:3001/ || exit 1 " ,
" 30s " , " 3 " ,
) ,
" filebrowser " = > (
" curl -sf http://localhost:80/health || exit 1 " ,
" 30s " , " 3 " ,
) ,
" searxng " = > (
" curl -sf http://localhost:8080/ || exit 1 " ,
" 30s " , " 3 " ,
) ,
" photoprism " = > (
" curl -sf http://localhost:2342/api/v1/status || exit 1 " ,
" 60s " , " 3 " ,
) ,
" immich_server " | " immich " = > (
" curl -sf http://localhost:2283/api/server/ping || exit 1 " ,
" 30s " , " 3 " ,
) ,
" dwn " = > (
" curl -sf http://localhost:3000/health || exit 1 " ,
" 30s " , " 3 " ,
) ,
" portainer " = > (
" curl -sf http://localhost:9000/api/status || exit 1 " ,
" 30s " , " 3 " ,
) ,
" ollama " = > (
" curl -sf http://localhost:11434/ || exit 1 " ,
" 30s " , " 3 " ,
) ,
" fedimint " = > (
" curl -sf http://localhost:8174/health || exit 1 " ,
" 60s " , " 3 " ,
) ,
" nostr-rs-relay " | " nostr-relay " = > (
" curl -sf http://localhost:8080/ || exit 1 " ,
" 30s " , " 3 " ,
) ,
" nginx-proxy-manager " = > (
" curl -sf http://localhost:81/api/ || exit 1 " ,
" 30s " , " 3 " ,
) ,
_ = > return vec! [ ] ,
} ;
vec! [
format! ( " --health-cmd= {} " , cmd ) ,
format! ( " --health-interval= {} " , interval ) ,
format! ( " --health-retries= {} " , retries ) ,
" --health-start-period=60s " . to_string ( ) ,
]
}
/// Get per-app memory limit.
fn get_memory_limit ( app_id : & str ) -> & 'static str {
match app_id {
// Heavy apps
" bitcoin " | " bitcoin-core " | " bitcoin-knots " = > " 2g " ,
" onlyoffice " | " onlyoffice-documentserver " = > " 2g " ,
" ollama " = > " 4g " ,
// Medium apps
" lnd " = > " 512m " ,
" mempool-electrs " | " electrs " = > " 1g " ,
" nextcloud " = > " 1g " ,
" immich_server " | " immich " = > " 1g " ,
" btcpay-server " | " btcpayserver " = > " 1g " ,
" homeassistant " | " home-assistant " = > " 512m " ,
" fedimint " = > " 512m " ,
" fedimint-gateway " = > " 512m " ,
" photoprism " = > " 1g " ,
// Light apps
" mempool-api " = > " 512m " ,
" mempool " | " mempool-web " | " archy-mempool-web " = > " 256m " ,
" grafana " = > " 256m " ,
" jellyfin " = > " 1g " ,
" vaultwarden " = > " 256m " ,
" uptime-kuma " = > " 256m " ,
" filebrowser " = > " 256m " ,
" searxng " = > " 512m " ,
" dwn " = > " 256m " ,
" portainer " = > " 256m " ,
" nostr-rs-relay " | " nostr-relay " = > " 256m " ,
" nginx-proxy-manager " = > " 256m " ,
// Databases
" archy-btcpay-db " | " archy-mempool-db " | " mysql-mempool " = > " 512m " ,
" immich_postgres " | " penpot-postgres " = > " 256m " ,
" immich_redis " | " penpot-valkey " = > " 128m " ,
// Default
_ = > " 512m " ,
}
}
2026-02-01 18:46:35 +00:00
/// Get app-specific configuration
2026-02-17 15:03:34 +00:00
/// Returns: (ports, volumes, env_vars, custom_command, custom_args)
fn get_app_config (
app_id : & str ,
host_ip : & str ,
allocator : & mut PortAllocator ,
) -> ( Vec < String > , Vec < String > , Vec < String > , Option < String > , Option < Vec < String > > ) {
2026-02-01 18:46:35 +00:00
match app_id {
" homeassistant " | " home-assistant " = > (
vec! [ " 8123:8123 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/home-assistant:/config " . to_string ( ) ] ,
vec! [ " TZ=UTC " . to_string ( ) ] ,
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
2026-02-25 18:04:41 +00:00
" bitcoin " | " bitcoin-core " | " bitcoin-knots " = > (
2026-02-01 18:46:35 +00:00
vec! [ " 8332:8332 " . to_string ( ) , " 8333:8333 " . to_string ( ) ] ,
2026-02-25 18:04:41 +00:00
vec! [ " /var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin " . to_string ( ) ] ,
2026-02-01 18:46:35 +00:00
vec! [ ] ,
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
" lnd " = > (
vec! [ " 9735:9735 " . to_string ( ) , " 10009:10009 " . to_string ( ) , " 8080:8080 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/lnd:/root/.lnd " . to_string ( ) ] ,
vec! [ " BITCOIN_ACTIVE=1 " . to_string ( ) ] ,
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
" btcpay-server " | " btcpayserver " = > (
vec! [ " 23000:49392 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/btcpay:/datadir " . to_string ( ) ] ,
2026-02-17 15:03:34 +00:00
vec! [
" ASPNETCORE_URLS=http://0.0.0.0:49392 " . to_string ( ) ,
" BTCPAY_PROTOCOL=http " . to_string ( ) ,
format! ( " BTCPAY_HOST= {} :23000 " , host_ip ) ,
" BTCPAY_CHAINS=btc " . to_string ( ) ,
format! ( " BTCPAY_BTCRPCURL=http:// {} :8332 " , host_ip ) ,
" BTCPAY_BTCRPCUSER=archipelago " . to_string ( ) ,
" BTCPAY_BTCRPCPASSWORD=archipelago123 " . to_string ( ) ,
" BTCPAY_POSTGRES=User ID=btcpay;Password=btcpaypass;Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true " . to_string ( ) ,
] ,
None ,
None ,
) ,
" mempool " | " mempool-web " = > (
vec! [ " 4080:8080 " . to_string ( ) ] ,
2026-02-01 18:46:35 +00:00
vec! [ ] ,
2026-02-17 15:03:34 +00:00
vec! [ format! ( " BACKEND_MAINNET_HTTP_HOST= {} " , host_ip ) ] ,
None ,
None ,
) ,
" mempool-api " = > (
vec! [ " 8999:8999 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/mempool:/data " . to_string ( ) ] ,
vec! [
" MEMPOOL_BACKEND=electrum " . to_string ( ) ,
" ELECTRUM_HOST=mempool-electrs " . to_string ( ) ,
" ELECTRUM_PORT=50001 " . to_string ( ) ,
" ELECTRUM_TLS_ENABLED=false " . to_string ( ) ,
format! ( " CORE_RPC_HOST= {} " , host_ip ) ,
" CORE_RPC_PORT=8332 " . to_string ( ) ,
2026-02-25 17:23:38 +00:00
" CORE_RPC_USERNAME=archipelago " . to_string ( ) ,
" CORE_RPC_PASSWORD=archipelago123 " . to_string ( ) ,
2026-02-17 15:03:34 +00:00
" DATABASE_ENABLED=true " . to_string ( ) ,
" DATABASE_HOST=archy-mempool-db " . to_string ( ) ,
" DATABASE_DATABASE=mempool " . to_string ( ) ,
" DATABASE_USERNAME=mempool " . to_string ( ) ,
" DATABASE_PASSWORD=mempoolpass " . to_string ( ) ,
] ,
None ,
2026-02-01 18:46:35 +00:00
None ,
) ,
2026-03-08 02:16:02 +00:00
" mempool-electrs " | " electrs " = > {
// Detect which bitcoin container is running for archy-net DNS resolution
let bitcoin_host = detect_bitcoin_container_name ( ) ;
(
vec! [ " 50001:50001 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/mempool-electrs:/data " . to_string ( ) ] ,
vec! [ ] ,
None ,
Some ( vec! [
" --daemon-rpc-addr " . to_string ( ) ,
format! ( " {} :8332 " , bitcoin_host ) ,
" --cookie " . to_string ( ) ,
" archipelago:archipelago123 " . to_string ( ) ,
" --jsonrpc-import " . to_string ( ) ,
" --electrum-rpc-addr " . to_string ( ) ,
" 0.0.0.0:50001 " . to_string ( ) ,
" --db-dir " . to_string ( ) ,
" /data " . to_string ( ) ,
" --lightmode " . to_string ( ) ,
] ) ,
)
} ,
2026-02-17 15:03:34 +00:00
" mysql-mempool " = > (
2026-02-01 18:46:35 +00:00
vec! [ ] ,
2026-02-17 15:03:34 +00:00
vec! [ " /var/lib/archipelago/mysql-mempool:/var/lib/mysql " . to_string ( ) ] ,
vec! [
" MYSQL_DATABASE=mempool " . to_string ( ) ,
" MYSQL_USER=mempool " . to_string ( ) ,
" MYSQL_PASSWORD=mempoolpass " . to_string ( ) ,
" MYSQL_ROOT_PASSWORD=rootpass " . to_string ( ) ,
] ,
None ,
2026-02-01 18:46:35 +00:00
None ,
) ,
" grafana " = > (
vec! [ " 3000:3000 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/grafana:/var/lib/grafana " . to_string ( ) ] ,
2026-02-25 18:04:41 +00:00
vec! [ " GF_PATHS_DATA=/var/lib/grafana " . to_string ( ) , " GF_USERS_ALLOW_SIGN_UP=false " . to_string ( ) ] ,
2026-02-01 18:46:35 +00:00
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
" searxng " = > (
vec! [ " 8888:8080 " . to_string ( ) ] ,
vec! [ ] ,
vec! [ ] ,
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
" ollama " = > (
vec! [ " 11434:11434 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/ollama:/root/.ollama " . to_string ( ) ] ,
vec! [ ] ,
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
" onlyoffice " | " onlyoffice-documentserver " = > (
vec! [ " 9980:80 " . to_string ( ) ] ,
vec! [ ] ,
vec! [ ] ,
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
" penpot " | " penpot-frontend " = > (
vec! [ " 9001:80 " . to_string ( ) ] ,
vec! [ ] ,
vec! [ ] ,
None ,
None ,
) ,
2026-02-17 15:03:34 +00:00
" nextcloud " = > {
let host_port = allocator
. allocate_or_get ( app_id , 8085 , 80 )
. unwrap_or ( 8085 ) ;
(
vec! [ format! ( " {} :80 " , host_port ) ] ,
vec! [ " /var/lib/archipelago/nextcloud:/var/www/html " . to_string ( ) ] ,
vec! [ ] ,
None ,
None ,
)
}
" vaultwarden " = > {
let host_port = allocator
. allocate_or_get ( app_id , 8082 , 80 )
. unwrap_or ( 8082 ) ;
(
vec! [ format! ( " {} :80 " , host_port ) ] ,
vec! [ " /var/lib/archipelago/vaultwarden:/data " . to_string ( ) ] ,
vec! [ ] ,
None ,
None ,
)
}
2026-02-01 18:46:35 +00:00
" jellyfin " = > (
vec! [ " 8096:8096 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/jellyfin/config:/config " . to_string ( ) , " /var/lib/archipelago/jellyfin/cache:/cache " . to_string ( ) ] ,
vec! [ ] ,
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
" photoprism " = > (
vec! [ " 2342:2342 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/photoprism:/photoprism/storage " . to_string ( ) ] ,
2026-02-25 18:04:41 +00:00
vec! [ " PHOTOPRISM_ADMIN_PASSWORD=archipelago " . to_string ( ) , " PHOTOPRISM_DEFAULT_LOCALE=en " . to_string ( ) ] ,
2026-02-01 18:46:35 +00:00
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
" immich " = > (
2026-02-25 18:04:41 +00:00
vec! [ " 2283:2283 " . to_string ( ) ] ,
2026-02-01 18:46:35 +00:00
vec! [ " /var/lib/archipelago/immich:/usr/src/app/upload " . to_string ( ) ] ,
2026-02-25 18:04:41 +00:00
vec! [
" DB_HOSTNAME=immich_postgres " . to_string ( ) ,
" DB_USERNAME=postgres " . to_string ( ) ,
" DB_PASSWORD=immichpass " . to_string ( ) ,
" DB_DATABASE_NAME=immich " . to_string ( ) ,
" REDIS_HOSTNAME=immich_redis " . to_string ( ) ,
" UPLOAD_LOCATION=/usr/src/app/upload " . to_string ( ) ,
] ,
2026-02-01 18:46:35 +00:00
None ,
None ,
) ,
2026-02-17 15:03:34 +00:00
" filebrowser " = > {
let host_port = allocator
. allocate_or_get ( app_id , 8083 , 80 )
. unwrap_or ( 8083 ) ;
(
vec! [ format! ( " {} :80 " , host_port ) ] ,
vec! [ " /var/lib/archipelago/filebrowser:/srv " . to_string ( ) ] ,
vec! [ ] ,
None ,
None ,
)
}
2026-02-01 18:46:35 +00:00
" nginx-proxy-manager " = > (
vec! [ " 81:81 " . to_string ( ) , " 8084:80 " . to_string ( ) , " 8443:443 " . to_string ( ) ] ,
vec! [
" /var/lib/archipelago/nginx-proxy-manager/data:/data " . to_string ( ) ,
" /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt " . to_string ( ) ,
] ,
vec! [ ] ,
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
" portainer " = > (
vec! [ " 9000:9000 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/portainer:/data " . to_string ( ) , " /var/run/podman/podman.sock:/var/run/docker.sock " . to_string ( ) ] ,
vec! [ ] ,
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
" uptime-kuma " = > (
vec! [ " 3001:3001 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/uptime-kuma:/app/data " . to_string ( ) ] ,
2026-02-25 18:04:41 +00:00
vec! [ " TZ=UTC " . to_string ( ) ] ,
2026-02-01 18:46:35 +00:00
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
" tailscale " = > (
2026-03-04 05:23:42 +00:00
vec! [ " 8240:8240 " . to_string ( ) ] ,
2026-02-01 18:46:35 +00:00
vec! [
" /var/lib/archipelago/tailscale:/var/lib/tailscale " . to_string ( ) ,
] ,
vec! [
" TS_STATE_DIR=/var/lib/tailscale " . to_string ( ) ,
] ,
Some ( " sh -c 'tailscale web --listen 0.0.0.0:8240 & exec tailscaled' " . to_string ( ) ) ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
" fedimint " = > (
2026-02-17 15:03:34 +00:00
vec! [
2026-03-04 05:23:42 +00:00
" 8173:8173 " . to_string ( ) ,
" 8174:8174 " . to_string ( ) ,
" 8175:8175 " . to_string ( ) ,
2026-02-17 15:03:34 +00:00
] ,
2026-02-01 18:46:35 +00:00
vec! [ " /var/lib/archipelago/fedimint:/data " . to_string ( ) ] ,
vec! [
2026-02-17 15:03:34 +00:00
" FM_DATA_DIR=/data " . to_string ( ) ,
2026-02-25 17:23:38 +00:00
" FM_BITCOIND_USERNAME=archipelago " . to_string ( ) ,
" FM_BITCOIND_PASSWORD=archipelago123 " . to_string ( ) ,
2026-02-17 15:03:34 +00:00
" FM_BITCOIN_NETWORK=bitcoin " . to_string ( ) ,
2026-02-01 18:46:35 +00:00
" FM_BIND_P2P=0.0.0.0:8173 " . to_string ( ) ,
" FM_BIND_API=0.0.0.0:8174 " . to_string ( ) ,
2026-02-17 15:03:34 +00:00
" FM_BIND_UI=0.0.0.0:8175 " . to_string ( ) ,
format! ( " FM_P2P_URL=fedimint:// {} :8173 " , host_ip ) ,
format! ( " FM_API_URL=ws:// {} :8174 " , host_ip ) ,
format! ( " FM_BITCOIND_URL=http:// {} :8332 " , host_ip ) ,
2026-02-01 18:46:35 +00:00
] ,
None ,
2026-02-17 15:03:34 +00:00
None ,
2026-02-01 18:46:35 +00:00
) ,
2026-03-09 07:43:12 +00:00
" fedimint-gateway " = > (
vec! [ " 8176:8176 " . to_string ( ) , " 9737:9737 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/fedimint-gateway:/data " . to_string ( ) ] ,
vec! [ ] ,
None ,
Some ( vec! [
" gatewayd " . to_string ( ) ,
" --data-dir " . to_string ( ) , " /data " . to_string ( ) ,
" --listen " . to_string ( ) , " 0.0.0.0:8176 " . to_string ( ) ,
" --bcrypt-password-hash " . to_string ( ) ,
" $2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC " . to_string ( ) ,
" --network " . to_string ( ) , " bitcoin " . to_string ( ) ,
" --bitcoind-url " . to_string ( ) , format! ( " http:// {} :8332 " , host_ip ) ,
" --bitcoind-username " . to_string ( ) , " archipelago " . to_string ( ) ,
" --bitcoind-password " . to_string ( ) , " archipelago123 " . to_string ( ) ,
" ldk " . to_string ( ) ,
" --ldk-lightning-port " . to_string ( ) , " 9737 " . to_string ( ) ,
" --ldk-alias " . to_string ( ) , " archipelago-gateway " . to_string ( ) ,
] ) ,
) ,
2026-03-01 17:53:18 +00:00
" indeedhub " = > (
vec! [ " 7777:7777 " . to_string ( ) ] ,
vec! [ ] ,
vec! [ " NGINX_HOST=0.0.0.0 " . to_string ( ) , " NGINX_PORT=7777 " . to_string ( ) ] ,
None ,
None ,
) ,
2026-03-09 07:43:12 +00:00
" nostr-rs-relay " = > (
vec! [ " 18081:8080 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/nostr-rs-relay:/usr/src/app/db " . to_string ( ) ] ,
vec! [ ] ,
None ,
None ,
) ,
" dwn " = > (
vec! [ " 3100:3000 " . to_string ( ) ] ,
vec! [ " /var/lib/archipelago/dwn:/dwn/data " . to_string ( ) ] ,
vec! [
" DS_PORT=3000 " . to_string ( ) ,
" DS_MESSAGES_STORE_URI=level://data/messages " . to_string ( ) ,
" DS_DATA_STORE_URI=level://data/data " . to_string ( ) ,
" DS_EVENT_LOG_URI=level://data/events " . to_string ( ) ,
] ,
None ,
None ,
) ,
2026-03-04 05:23:42 +00:00
_ = > ( vec! [ ] , vec! [ ] , vec! [ ] , None , None ) ,
2026-02-01 18:46:35 +00:00
}
}