feat(registry): move image and OTA references to the public domain
Demo images / Build & push demo images (push) Failing after 2m22s

Replaces the registry host across 86 files: 309 references, covering all 40
app manifests, the orchestrator and container crates, the release and catalog
scripts, both demo-images workflows, the ISO builder, demo-deploy, and the
frontend marketplace data.

Verified the domain actually serves the registry before rewriting anything,
rather than assuming the web host implies the registry:
- TLS verifies clean, HTTP/2 on the web root
- an anonymous token grants a manifest fetch (HTTP 200) with no credentials
- skopeo inspect --no-creds resolves an image and lists its tags

That last check is the one that matters: an outside developer with no account
can now pull, which was the functional blocker for publishing at all.

Plain-HTTP references become HTTPS in the same pass, so OTA downloads stop
crossing the network in the clear.

Deliberately NOT rewritten:
- The public FIPS anchor on port 8444. It is a functional network endpoint
  every node dials to bootstrap the mesh — closer to Bitcoin Core's hardcoded
  seeds than to leaked infrastructure. The domain does resolve to the same
  host, so it could become a hostname, but that adds a DNS dependency to the
  path used precisely when things are broken. Worth a deliberate decision,
  not a side effect of this change.
- The companion APK on port 2100. The domain returns 404 for that path, so
  rewriting it would swap a working URL for a broken one. The Releases page
  does serve (200), which is where the plan already wants those binaries.
- releases/app-catalog.json, releases/manifest.json and release-manifest.json.
  These carry `signature` and `signed_by`; editing their contents invalidates
  the signature and the fleet refuses artifacts that fail verification. They
  were rewritten in a first pass and reverted — they must be regenerated and
  re-signed through the signing ceremony instead, which needs the mnemonic.

So the catalog still advertises the old host until that ceremony runs. Nodes
resolve images through the signed catalog, not the on-disk manifests, so this
commit alone does not change what a node pulls.

Verified: archipelago-container 75/75; every manifest still parses with a
top-level app block; no signed artifact modified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-07 11:31:20 -04:00
co-authored by Claude Opus 5
parent b2c7592840
commit 8e814ca06a
86 changed files with 210 additions and 210 deletions
+1 -1
View File
@@ -152,7 +152,7 @@ impl ApiHandler {
}
if upstreams.is_empty() {
upstreams.push(
"http://146.59.87.168:3000/lfg2025/app-catalog/raw/branch/main/catalog.json"
"https://source.archipelago-foundation.org/lfg2025/app-catalog/raw/branch/main/catalog.json"
.to_string(),
);
}
@@ -1925,7 +1925,7 @@ autopilot.active=false\n",
.unwrap_or(true);
// Registries are configured as `host[:port]/namespace` (for
// example `146.59.87.168:3000/lfg2025`), but the Docker V2
// example `source.archipelago-foundation.org/lfg2025`), but the Docker V2
// registry API lives at `/v2/` on the ROOT of the host — NOT
// under the namespace. Strip the namespace before appending
// `/v2/` so the reachability probe hits the correct URL.
@@ -335,15 +335,15 @@ mod tests {
#[test]
fn image_tag_keeps_registry_port_colon() {
assert_eq!(
image_tag("146.59.87.168:3000/lfg2025/bitcoin:28.4").as_deref(),
image_tag("source.archipelago-foundation.org/lfg2025/bitcoin:28.4").as_deref(),
Some("28.4")
);
assert_eq!(
image_tag("146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260508").as_deref(),
image_tag("source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260508").as_deref(),
Some("29.3.knots20260508")
);
// No tag => None (don't mistake the registry port for a tag).
assert_eq!(image_tag("146.59.87.168:3000/lfg2025/bitcoin"), None);
assert_eq!(image_tag("source.archipelago-foundation.org/lfg2025/bitcoin"), None);
assert_eq!(
image_tag("docker.io/library/redis:7"),
Some("7".to_string())
@@ -774,7 +774,7 @@ fn indeedhub_stack_app_ids() -> &'static [&'static str] {
]
}
const REGISTRY: &str = "146.59.87.168:3000/lfg2025";
const REGISTRY: &str = "source.archipelago-foundation.org/lfg2025";
/// Pull an image with retry and exponential backoff (3 attempts).
async fn pull_image_with_retry(image: &str) -> Result<()> {
@@ -871,9 +871,9 @@ impl RpcHandler {
}
let images = [
"146.59.87.168:3000/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0",
"146.59.87.168:3000/lfg2025/valkey:7-alpine",
"146.59.87.168:3000/lfg2025/immich-server:release",
"source.archipelago-foundation.org/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0",
"source.archipelago-foundation.org/lfg2025/valkey:7-alpine",
"source.archipelago-foundation.org/lfg2025/immich-server:release",
];
self.set_install_phase("immich", InstallPhase::PullingImage)
.await;
@@ -947,7 +947,7 @@ impl RpcHandler {
"POSTGRES_USER=postgres",
"-e",
"POSTGRES_DB=immich",
"146.59.87.168:3000/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0",
"source.archipelago-foundation.org/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0",
]);
run_required_stack_command("immich", "create postgres", &mut postgres_cmd).await?;
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
@@ -973,7 +973,7 @@ impl RpcHandler {
"--health-cmd=valkey-cli ping || exit 1",
"--health-interval=30s",
"--health-retries=3",
"146.59.87.168:3000/lfg2025/valkey:7-alpine",
"source.archipelago-foundation.org/lfg2025/valkey:7-alpine",
]);
run_required_stack_command("immich", "create redis", &mut redis_cmd).await?;
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
@@ -1016,7 +1016,7 @@ impl RpcHandler {
"REDIS_HOSTNAME=immich_redis".to_string(),
"-e".to_string(),
"UPLOAD_LOCATION=/usr/src/app/upload".to_string(),
"146.59.87.168:3000/lfg2025/immich-server:release".to_string(),
"source.archipelago-foundation.org/lfg2025/immich-server:release".to_string(),
],
)
.await?;
@@ -1517,7 +1517,7 @@ impl RpcHandler {
.into_iter()
.find(|r| r.enabled)
.map(|r| r.url)
.unwrap_or_else(|| "146.59.87.168:3000/lfg2025".to_string());
.unwrap_or_else(|| "source.archipelago-foundation.org/lfg2025".to_string());
let user_tmp = format!(
"{}/.local/share/containers/tmp",
@@ -546,7 +546,7 @@ mod tests {
"apps": {
"indeedhub": {
"version": "1.0.1",
"image": "146.59.87.168:3000/lfg2025/indeedhub:1.0.1",
"image": "source.archipelago-foundation.org/lfg2025/indeedhub:1.0.1",
"digest": "blake3:deadbeef",
"size": 12345,
"another_future_field": true
@@ -560,7 +560,7 @@ mod tests {
assert_eq!(e.version, "1.0.1");
assert_eq!(
e.image.as_deref(),
Some("146.59.87.168:3000/lfg2025/indeedhub:1.0.1")
Some("source.archipelago-foundation.org/lfg2025/indeedhub:1.0.1")
);
assert_eq!(e.digest.as_deref(), Some("blake3:deadbeef"));
}
@@ -628,7 +628,7 @@ mod tests {
#[test]
fn catalog_url_derived_from_mirror() {
let mirrors = vec![crate::update::UpdateMirror {
url: "http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/releases/manifest.json"
url: "https://source.archipelago-foundation.org/lfg2025/archy/raw/branch/main/releases/manifest.json"
.to_string(),
label: "Server 1".to_string(),
}];
@@ -636,7 +636,7 @@ mod tests {
assert_eq!(
urls,
vec![
"http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/releases/app-catalog.json"
"https://source.archipelago-foundation.org/lfg2025/archy/raw/branch/main/releases/app-catalog.json"
.to_string()
]
);
+2 -2
View File
@@ -33,7 +33,7 @@ use tracing::{info, warn};
use crate::container::quadlet::{self, BindMount, NetworkMode, QuadletUnit};
use archipelago_container::image_uses_insecure_registry;
const COMPANION_REGISTRY: &str = "146.59.87.168:3000/lfg2025";
const COMPANION_REGISTRY: &str = "source.archipelago-foundation.org/lfg2025";
const COMPANION_IMAGE_CHECK_TIMEOUT: Duration = Duration::from_secs(15);
const COMPANION_BUILD_TIMEOUT: Duration = Duration::from_secs(900);
const COMPANION_PULL_TIMEOUT: Duration = Duration::from_secs(300);
@@ -54,7 +54,7 @@ pub struct CompanionSpec {
/// Container + unit name (e.g. "archy-bitcoin-ui").
pub name: &'static str,
/// Image base name in the lfg2025 registry namespace
/// (e.g. "bitcoin-ui" → "146.59.87.168:3000/lfg2025/bitcoin-ui:latest").
/// (e.g. "bitcoin-ui" → "source.archipelago-foundation.org/lfg2025/bitcoin-ui:latest").
pub image_base: &'static str,
/// Filesystem locations to look for a local Dockerfile (build wins
/// over registry pull). Searched in order; first hit wins.
@@ -8,13 +8,13 @@
/// (git.tx1138.com was removed 2026-07-10: the host is retired and must
/// never be pulled through again.)
pub const TRUSTED_REGISTRIES: &[&str] =
&["docker.io", "ghcr.io", "localhost", "146.59.87.168:3000"];
&["docker.io", "ghcr.io", "localhost", "source.archipelago-foundation.org"];
/// Validate a container image reference.
///
/// Accepts:
/// * refs whose explicit registry host is on [`TRUSTED_REGISTRIES`]
/// (`docker.io/grafana/grafana`, `146.59.87.168:3000/archy/x:1`), and
/// (`docker.io/grafana/grafana`, `source.archipelago-foundation.org/archy/x:1`), and
/// * registry-less Docker Hub shorthand (`nginx`, `grafana/grafana`) —
/// the first segment has no `.`/`:` so it cannot name an attacker host;
/// resolution follows the host's registries.conf search order.
@@ -55,7 +55,7 @@ mod tests {
"docker.io/library/nginx:1.25",
"ghcr.io/owner/app:latest",
"localhost/archy-dev:1",
"146.59.87.168:3000/archy/bitcoin-knots:28.1",
"source.archipelago-foundation.org/archy/bitcoin-knots:28.1",
] {
assert!(is_valid_docker_image(img), "{img} should be accepted");
}
@@ -234,7 +234,7 @@ pub fn available_update_for_images(pinned: &str, running_image: &str) -> Option<
}
/// Extract version tag from a full image reference.
/// e.g. "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta" → "v0.18.4-beta"
/// e.g. "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta" → "v0.18.4-beta"
/// Returns "latest" if no tag or tag is empty.
pub fn extract_version_from_image(image: &str) -> String {
// Split off the tag after the last colon, but only if it comes after the last slash
@@ -328,11 +328,11 @@ mod tests {
#[test]
fn test_extract_version() {
assert_eq!(
extract_version_from_image("146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta"),
extract_version_from_image("source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta"),
"v0.18.4-beta"
);
assert_eq!(
extract_version_from_image("146.59.87.168:3000/lfg2025/grafana:10.2.0"),
extract_version_from_image("source.archipelago-foundation.org/lfg2025/grafana:10.2.0"),
"10.2.0"
);
assert_eq!(
@@ -340,7 +340,7 @@ mod tests {
"latest"
);
assert_eq!(
extract_version_from_image("146.59.87.168:3000/lfg2025/bitcoin-knots:latest"),
extract_version_from_image("source.archipelago-foundation.org/lfg2025/bitcoin-knots:latest"),
"latest"
);
}
@@ -348,11 +348,11 @@ mod tests {
#[test]
fn strips_registry_and_tag_for_image_identity() {
assert_eq!(
image_without_registry_or_tag("146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta"),
image_without_registry_or_tag("source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta"),
"lfg2025/lnd"
);
assert_eq!(
image_without_registry_or_tag("146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta"),
image_without_registry_or_tag("source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta"),
"lfg2025/lnd"
);
}
@@ -368,8 +368,8 @@ mod tests {
fn available_update_ignores_registry_only_changes() {
assert_eq!(
available_update_for_images(
"146.59.87.168:3000/lfg2025/nextcloud:29",
"146.59.87.168:3000/lfg2025/nextcloud:29",
"source.archipelago-foundation.org/lfg2025/nextcloud:29",
"source.archipelago-foundation.org/lfg2025/nextcloud:29",
),
None
);
@@ -379,8 +379,8 @@ mod tests {
fn available_update_returns_pinned_version_for_same_repo_newer_tag() {
assert_eq!(
available_update_for_images(
"146.59.87.168:3000/lfg2025/nextcloud:29",
"146.59.87.168:3000/lfg2025/nextcloud:28",
"source.archipelago-foundation.org/lfg2025/nextcloud:29",
"source.archipelago-foundation.org/lfg2025/nextcloud:28",
),
Some("29".to_string())
);
@@ -389,7 +389,7 @@ mod tests {
#[test]
fn test_parse_image_versions() {
let content = r#"
ARCHY_REGISTRY="146.59.87.168:3000/lfg2025"
ARCHY_REGISTRY="source.archipelago-foundation.org/lfg2025"
LND_IMAGE="$ARCHY_REGISTRY/lnd:v0.18.4-beta"
GRAFANA_IMAGE="$ARCHY_REGISTRY/grafana:10.2.0"
# comment
@@ -398,11 +398,11 @@ NOT_AN_IMAGE="something"
let parsed = parse_image_versions(content);
assert_eq!(
parsed.get("LND_IMAGE"),
Some(&"146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta".to_string())
Some(&"source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta".to_string())
);
assert_eq!(
parsed.get("GRAFANA_IMAGE"),
Some(&"146.59.87.168:3000/lfg2025/grafana:10.2.0".to_string())
Some(&"source.archipelago-foundation.org/lfg2025/grafana:10.2.0".to_string())
);
assert!(!parsed.contains_key("NOT_AN_IMAGE"));
assert!(!parsed.contains_key("ARCHY_REGISTRY"));
@@ -5224,7 +5224,7 @@ app:
name: File Browser
version: 1.0.0
container:
image: 146.59.87.168:3000/lfg2025/filebrowser:v2.27.0
image: source.archipelago-foundation.org/lfg2025/filebrowser:v2.27.0
custom_args:
- --config
- /data/.filebrowser.json
@@ -5248,7 +5248,7 @@ app:
name: LND
version: 1.0.0
container:
image: 146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta
image: source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta
secret_env:
- key: BITCOIND_RPCPASS
secret_file: bitcoin-rpc-password
+2 -2
View File
@@ -956,7 +956,7 @@ mod tests {
QuadletUnit {
name: "archy-bitcoin-ui".into(),
description: "Bitcoin RPC UI proxy".into(),
image: "146.59.87.168:3000/lfg2025/bitcoin-ui:1.7.84-alpha".into(),
image: "source.archipelago-foundation.org/lfg2025/bitcoin-ui:1.7.84-alpha".into(),
network: NetworkMode::Host,
user: Some("0:0".into()),
memory_mb: Some(128),
@@ -984,7 +984,7 @@ mod tests {
let s = sample_unit().render();
assert!(s.contains("[Container]"));
assert!(s.contains("ContainerName=archy-bitcoin-ui"));
assert!(s.contains("Image=146.59.87.168:3000/lfg2025/bitcoin-ui:1.7.84-alpha"));
assert!(s.contains("Image=source.archipelago-foundation.org/lfg2025/bitcoin-ui:1.7.84-alpha"));
assert!(s.contains("Pull=never"));
assert!(s.contains("Network=host"));
assert!(s.contains("DropCapability=ALL"));
+7 -7
View File
@@ -10,7 +10,7 @@ use std::path::Path;
use tokio::fs;
const REGISTRY_FILE: &str = "config/registries.json";
const OVH_REGISTRY_URL: &str = "146.59.87.168:3000/lfg2025";
const OVH_REGISTRY_URL: &str = "source.archipelago-foundation.org/lfg2025";
/// Retired registry host (release server retired 2026-06-13; the registry
/// frontend was fully dead by 2026-07-10 — 500 on every /v2 manifest read).
/// Never a default, never force-enabled; stripped from saved configs on
@@ -21,7 +21,7 @@ const RETIRED_TX1138_HOST: &str = "git.tx1138.com";
/// A single container registry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Registry {
/// Registry URL (e.g., "146.59.87.168:3000/lfg2025").
/// Registry URL (e.g., "source.archipelago-foundation.org/lfg2025").
pub url: String,
/// Human-readable name.
pub name: String,
@@ -68,8 +68,8 @@ impl RegistryConfig {
}
/// Rewrite an image reference to use a specific registry.
/// E.g., "docker.io/lfg2025/bitcoin-knots:latest" with registry "146.59.87.168:3000/lfg2025"
/// becomes "146.59.87.168:3000/lfg2025/bitcoin-knots:latest".
/// E.g., "docker.io/lfg2025/bitcoin-knots:latest" with registry "source.archipelago-foundation.org/lfg2025"
/// becomes "source.archipelago-foundation.org/lfg2025/bitcoin-knots:latest".
pub fn rewrite_image(&self, image: &str, registry: &Registry) -> String {
// Extract the image name (last component after the org/namespace)
// Handles: "registry/org/image:tag" -> "image:tag"
@@ -79,7 +79,7 @@ impl RegistryConfig {
}
/// Extract the image name from a full image reference.
/// "146.59.87.168:3000/lfg2025/bitcoin-knots:latest" -> "bitcoin-knots:latest"
/// "source.archipelago-foundation.org/lfg2025/bitcoin-knots:latest" -> "bitcoin-knots:latest"
/// "docker.io/gitea/gitea:1.23" -> "gitea:1.23"
fn extract_image_name(image: &str) -> &str {
// Split by '/' and take the last segment (image:tag)
@@ -212,7 +212,7 @@ mod tests {
#[test]
fn test_extract_image_name() {
assert_eq!(
extract_image_name("146.59.87.168:3000/lfg2025/bitcoin-knots:latest"),
extract_image_name("source.archipelago-foundation.org/lfg2025/bitcoin-knots:latest"),
"bitcoin-knots:latest"
);
assert_eq!(
@@ -230,7 +230,7 @@ mod tests {
let primary = &config.registries[0];
assert_eq!(
config.rewrite_image("docker.io/lfg2025/bitcoin-knots:latest", primary),
"146.59.87.168:3000/lfg2025/bitcoin-knots:latest"
"source.archipelago-foundation.org/lfg2025/bitcoin-knots:latest"
);
}
+1 -1
View File
@@ -88,7 +88,7 @@ const DEFAULT_UPDATE_MANIFEST_URL: &str =
/// whose DNS or TLS is broken still updates. Dropped from the mirror list
/// once the fleet has moved.
const LEGACY_UPDATE_MANIFEST_URL: &str =
"http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/releases/manifest.json";
"https://source.archipelago-foundation.org/lfg2025/archy/raw/branch/main/releases/manifest.json";
const UPDATE_STATE_FILE: &str = "update_state.json";
const UPDATE_MIRRORS_FILE: &str = "update-mirrors.json";
/// Marker written by apply_update() just before the service restart and