chore: purge retired git.tx1138.com registry host from the codebase

The tx1138 Gitea was retired as a release server 2026-06-13 and its registry
frontend is fully dead (500 on every /v2 manifest read, observed 2026-07-10).
Nothing may reference it anymore:

- registry.rs: no longer a default registry, no longer force-enabled on
  load; saved configs are stripped of it on load (same one-time migration
  treatment as the decommissioned Hetzner mirror), with a regression test.
- image_policy.rs: removed from TRUSTED_REGISTRIES — refs through the dead
  host are now refused at the pull site (rejection test added).
- api/handler: dropped the legacy catalog-proxy fallback URL.
- .gitmodules: indeedhub submodule repointed to the OVH Gitea.
- scripts, image-recipe, app-catalog data, neode-ui strings, docs, and all
  test fixtures repointed to 146.59.87.168:3000 (or neutral example hosts).
- image-versions.sh: ARCHY_REGISTRY_FALLBACK emptied (guarded consumers
  skip it); reconcile-containers.sh candidate guard hardened.

The only remaining occurrences of the host string are the strip/reject
enforcement paths and their regression tests — the code that guarantees it
is never used again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-07-10 18:55:32 +01:00
co-authored by Claude Fable 5
parent 99529fcce5
commit 4b0e44fc6c
29 changed files with 140 additions and 101 deletions
+5 -9
View File
@@ -126,15 +126,15 @@ impl ApiHandler {
}
/// Server-side fetch of the upstream app catalog so the browser can
/// load it without fighting CORS (git.tx1138.com emits no ACAO) or
/// load it without fighting CORS (upstream Gitea emits no ACAO) or
/// CSP (the fallback IP-port URL isn't in `connect-src`). The upstream
/// list is derived from the operator's configured container registries
/// so switching mirrors in Settings changes the App Store source too —
/// each active registry contributes one Gitea `raw/branch/main/catalog.json`
/// URL (http or https per `tls_verify`), tried in priority order.
/// If registry config can't be loaded, falls back to the legacy
/// hardcoded pair so the App Store still renders on nodes that haven't
/// persisted a registry config yet. 15s total timeout.
/// If registry config can't be loaded, falls back to the hardcoded OVH
/// URL so the App Store still renders on nodes that haven't persisted
/// a registry config yet. 15s total timeout.
async fn handle_app_catalog_proxy(&self) -> Result<Response<hyper::Body>> {
let mut upstreams: Vec<String> = Vec::new();
if let Ok(config) = crate::container::registry::load_registries(&self.config.data_dir).await
@@ -155,10 +155,6 @@ impl ApiHandler {
"http://146.59.87.168:3000/lfg2025/app-catalog/raw/branch/main/catalog.json"
.to_string(),
);
upstreams.push(
"https://git.tx1138.com/lfg2025/app-catalog/raw/branch/main/catalog.json"
.to_string(),
);
}
let client = match reqwest::Client::builder()
@@ -527,7 +523,7 @@ impl ApiHandler {
// App-catalog proxy — fetches catalog.json from the configured
// upstream URLs server-side so the browser doesn't hit CORS
// (git.tx1138.com has no ACAO header) or CSP (IP-port upstream
// (upstream Gitea has no ACAO header) or CSP (IP-port upstream
// falls outside `connect-src`). Session-authenticated so only
// the logged-in node owner can spin up fetches.
(Method::GET, "/api/app-catalog") => {
@@ -5,11 +5,12 @@
//! 1.8.0 hardening plan).
/// Registries images may be pulled from with an explicit host part.
/// (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",
"git.tx1138.com",
"146.59.87.168:3000",
];
@@ -58,13 +59,19 @@ mod tests {
"docker.io/library/nginx:1.25",
"ghcr.io/owner/app:latest",
"localhost/archy-dev:1",
"git.tx1138.com/lfg2025/x:2",
"146.59.87.168:3000/archy/bitcoin-knots:28.1",
] {
assert!(is_valid_docker_image(img), "{img} should be accepted");
}
}
#[test]
fn rejects_retired_tx1138_registry() {
// Retired 2026-07-10 — refs through the dead host must be refused
// at the pull site, not time out against it.
assert!(!is_valid_docker_image("git.tx1138.com/lfg2025/x:2"));
}
#[test]
fn accepts_docker_hub_shorthand() {
for img in ["nginx", "grafana/grafana:11.2.0", "lightninglabs/lnd:v0.18"] {
@@ -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. "git.tx1138.com/lfg2025/lnd:v0.18.4-beta" → "v0.18.4-beta"
/// e.g. "146.59.87.168:3000/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("git.tx1138.com/lfg2025/lnd:v0.18.4-beta"),
extract_version_from_image("146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta"),
"v0.18.4-beta"
);
assert_eq!(
extract_version_from_image("git.tx1138.com/lfg2025/grafana:10.2.0"),
extract_version_from_image("146.59.87.168:3000/lfg2025/grafana:10.2.0"),
"10.2.0"
);
assert_eq!(
@@ -340,7 +340,7 @@ mod tests {
"latest"
);
assert_eq!(
extract_version_from_image("git.tx1138.com/lfg2025/bitcoin-knots:latest"),
extract_version_from_image("146.59.87.168:3000/lfg2025/bitcoin-knots:latest"),
"latest"
);
}
@@ -352,7 +352,7 @@ mod tests {
"lfg2025/lnd"
);
assert_eq!(
image_without_registry_or_tag("git.tx1138.com/lfg2025/lnd:v0.18.4-beta"),
image_without_registry_or_tag("146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta"),
"lfg2025/lnd"
);
}
@@ -369,7 +369,7 @@ mod tests {
assert_eq!(
available_update_for_images(
"146.59.87.168:3000/lfg2025/nextcloud:29",
"git.tx1138.com/lfg2025/nextcloud:29",
"146.59.87.168:3000/lfg2025/nextcloud:29",
),
None
);
@@ -389,7 +389,7 @@ mod tests {
#[test]
fn test_parse_image_versions() {
let content = r#"
ARCHY_REGISTRY="git.tx1138.com/lfg2025"
ARCHY_REGISTRY="146.59.87.168:3000/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(&"git.tx1138.com/lfg2025/lnd:v0.18.4-beta".to_string())
Some(&"146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta".to_string())
);
assert_eq!(
parsed.get("GRAFANA_IMAGE"),
Some(&"git.tx1138.com/lfg2025/grafana:10.2.0".to_string())
Some(&"146.59.87.168:3000/lfg2025/grafana:10.2.0".to_string())
);
assert!(!parsed.contains_key("NOT_AN_IMAGE"));
assert!(!parsed.contains_key("ARCHY_REGISTRY"));
@@ -2251,7 +2251,7 @@ impl ProdContainerOrchestrator {
// Reconcile recreates route through here too: an unreachable
// registry must not brick an app whose exact image:tag is
// already in local storage (boot reconcile of archy-btcpay-db
// /archy-nbxplorer with git.tx1138.com dead, 2026-07-10).
// /archy-nbxplorer with the (since-retired) upstream registry dead, 2026-07-10).
// Same exists-first semantics as
// ensure_resolved_source_available and the quadlets'
// Pull=never.
@@ -4848,7 +4848,7 @@ app:
name: File Browser
version: 1.0.0
container:
image: git.tx1138.com/lfg2025/filebrowser:v2.27.0
image: 146.59.87.168:3000/lfg2025/filebrowser:v2.27.0
custom_args:
- --config
- /data/.filebrowser.json
@@ -4872,7 +4872,7 @@ app:
name: LND
version: 1.0.0
container:
image: git.tx1138.com/lfg2025/lnd:v0.18.4-beta
image: 146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta
secret_env:
- key: BITCOIND_RPCPASS
secret_file: bitcoin-rpc-password
@@ -4959,7 +4959,7 @@ app:
async fn install_fresh_skips_pull_when_image_local() {
// An unreachable registry must not brick a reconcile recreate whose
// exact image:tag is already in local storage (archy-btcpay-db /
// archy-nbxplorer boot reconcile with git.tx1138.com dead,
// archy-nbxplorer boot reconcile with the (since-retired) upstream registry dead,
// 2026-07-10).
let rt = Arc::new(MockRuntime::default());
rt.mark_image_present("docker.io/bitcoin/knots:28");
+66 -34
View File
@@ -11,12 +11,17 @@ use tokio::fs;
const REGISTRY_FILE: &str = "config/registries.json";
const OVH_REGISTRY_URL: &str = "146.59.87.168:3000/lfg2025";
const TX1138_REGISTRY_URL: &str = "git.tx1138.com/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
/// load. The literal exists ONLY so the strip can match — nothing may pull
/// through this host.
const RETIRED_TX1138_HOST: &str = "git.tx1138.com";
/// A single container registry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Registry {
/// Registry URL (e.g., "git.tx1138.com/lfg2025" or "146.59.87.168:3000/lfg2025").
/// Registry URL (e.g., "146.59.87.168:3000/lfg2025").
pub url: String,
/// Human-readable name.
pub name: String,
@@ -43,22 +48,13 @@ pub struct RegistryConfig {
impl Default for RegistryConfig {
fn default() -> Self {
Self {
registries: vec![
Registry {
url: OVH_REGISTRY_URL.to_string(),
name: "Server 1 (OVH)".to_string(),
tls_verify: false,
enabled: true,
priority: 0,
},
Registry {
url: TX1138_REGISTRY_URL.to_string(),
name: "Server 2 (tx1138)".to_string(),
tls_verify: true,
enabled: true,
priority: 10,
},
],
registries: vec![Registry {
url: OVH_REGISTRY_URL.to_string(),
name: "Server 1 (OVH)".to_string(),
tls_verify: false,
enabled: true,
priority: 0,
}],
}
}
}
@@ -72,7 +68,7 @@ impl RegistryConfig {
}
/// Rewrite an image reference to use a specific registry.
/// E.g., "git.tx1138.com/lfg2025/bitcoin-knots:latest" with registry "146.59.87.168:3000/lfg2025"
/// 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".
pub fn rewrite_image(&self, image: &str, registry: &Registry) -> String {
// Extract the image name (last component after the org/namespace)
@@ -83,7 +79,7 @@ impl RegistryConfig {
}
/// Extract the image name from a full image reference.
/// "git.tx1138.com/lfg2025/bitcoin-knots:latest" -> "bitcoin-knots:latest"
/// "146.59.87.168:3000/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)
@@ -118,6 +114,12 @@ pub async fn load_registries(data_dir: &Path) -> Result<RegistryConfig> {
config
.registries
.retain(|r| !r.url.contains("23.182.128.160"));
// Same treatment for the retired tx1138 registry (was Server 2 in older
// defaults): strip it on load so nothing ever pulls through the dead
// host again.
config
.registries
.retain(|r| !r.url.contains(RETIRED_TX1138_HOST));
let mut changed = config.registries.len() != before;
// Migrate: any default registry URL that isn't already in the
@@ -178,12 +180,6 @@ fn force_ovh_registry_primary(config: &mut RegistryConfig) {
registry.enabled = true;
registry.priority = 0;
}
TX1138_REGISTRY_URL => {
registry.name = "Server 2 (tx1138)".to_string();
registry.tls_verify = true;
registry.enabled = true;
registry.priority = 10;
}
_ => {
if registry.priority <= 10 {
registry.priority = registry.priority.saturating_add(20);
@@ -216,7 +212,7 @@ mod tests {
#[test]
fn test_extract_image_name() {
assert_eq!(
extract_image_name("git.tx1138.com/lfg2025/bitcoin-knots:latest"),
extract_image_name("146.59.87.168:3000/lfg2025/bitcoin-knots:latest"),
"bitcoin-knots:latest"
);
assert_eq!(
@@ -229,11 +225,11 @@ mod tests {
#[test]
fn test_rewrite_image() {
let config = RegistryConfig::default();
// Default primary is now the OVH VPS (index 0). A tx1138-hardcoded
// image rewrites to OVH when asked for the primary mirror.
// An image hardcoded to some other registry rewrites to OVH when
// asked for the primary mirror.
let primary = &config.registries[0];
assert_eq!(
config.rewrite_image("git.tx1138.com/lfg2025/bitcoin-knots:latest", primary),
config.rewrite_image("docker.io/lfg2025/bitcoin-knots:latest", primary),
"146.59.87.168:3000/lfg2025/bitcoin-knots:latest"
);
}
@@ -242,15 +238,51 @@ mod tests {
fn test_active_registries_sorted() {
let config = RegistryConfig::default();
let active = config.active_registries();
assert_eq!(active.len(), 2);
assert!(active[0].priority <= active[1].priority);
assert_eq!(active.len(), 1);
assert_eq!(active[0].url, OVH_REGISTRY_URL);
}
#[tokio::test]
async fn test_load_default() {
let tmp = TempDir::new().unwrap();
let config = load_registries(tmp.path()).await.unwrap();
assert_eq!(config.registries.len(), 2);
assert_eq!(config.registries.len(), 1);
}
#[tokio::test]
async fn test_load_strips_retired_tx1138_registry() {
// Nodes provisioned before the retirement have the tx1138 registry
// baked into their saved config (was Server 2). It must be stripped
// on load and never re-added by the defaults merge.
let tmp = TempDir::new().unwrap();
let config = RegistryConfig {
registries: vec![
Registry {
url: format!("{RETIRED_TX1138_HOST}/lfg2025"),
name: "Server 2 (tx1138)".into(),
tls_verify: true,
enabled: true,
priority: 10,
},
Registry {
url: OVH_REGISTRY_URL.into(),
name: "Server 1 (OVH)".into(),
tls_verify: false,
enabled: true,
priority: 0,
},
],
};
save_registries(tmp.path(), &config).await.unwrap();
let loaded = load_registries(tmp.path()).await.unwrap();
assert!(
!loaded
.registries
.iter()
.any(|r| r.url.contains(RETIRED_TX1138_HOST)),
"retired tx1138 registry must be stripped on load; got {:?}",
loaded.registries
);
}
#[tokio::test]
@@ -266,6 +298,6 @@ mod tests {
});
save_registries(tmp.path(), &config).await.unwrap();
let loaded = load_registries(tmp.path()).await.unwrap();
assert_eq!(loaded.registries.len(), 3);
assert_eq!(loaded.registries.len(), 2);
}
}
+8 -6
View File
@@ -2132,9 +2132,9 @@ mod tests {
fn test_manifest_origin_parses_https() {
assert_eq!(
manifest_origin(
"https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json"
"https://releases.example.com/lfg2025/archy/raw/branch/main/releases/manifest.json"
),
Some("https://git.tx1138.com".to_string())
Some("https://releases.example.com".to_string())
);
}
@@ -2151,7 +2151,7 @@ mod tests {
#[test]
fn test_manifest_origin_rejects_garbage() {
assert_eq!(manifest_origin("not a url"), None);
assert_eq!(manifest_origin("ftp://git.tx1138.com/x"), None);
assert_eq!(manifest_origin("ftp://releases.example.com/x"), None);
}
#[test]
@@ -2165,7 +2165,7 @@ mod tests {
name: "archipelago".into(),
current_version: "1.7.25-alpha".into(),
new_version: "1.7.26-alpha".into(),
download_url: "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/archipelago".into(),
download_url: "https://releases.example.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/archipelago".into(),
sha256: "x".into(),
size_bytes: 1,
blake3: None,
@@ -2174,7 +2174,7 @@ mod tests {
name: "frontend".into(),
current_version: "1.7.25-alpha".into(),
new_version: "1.7.26-alpha".into(),
download_url: "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/frontend.tar.gz".into(),
download_url: "https://releases.example.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/frontend.tar.gz".into(),
sha256: "y".into(),
size_bytes: 2,
blake3: None,
@@ -2218,6 +2218,8 @@ mod tests {
label: "Server 1 (OVH)".to_string(),
},
UpdateMirror {
// Deliberately the retired host: this fixture exists to prove
// load_mirrors strips it.
url: "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json"
.to_string(),
label: "Server 2 (tx1138)".to_string(),
@@ -2406,7 +2408,7 @@ mod tests {
rollback_available: false,
schedule: UpdateSchedule::Manual,
manifest_mirror: Some(
"https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json"
"https://releases.example.com/lfg2025/archy/raw/branch/main/releases/manifest.json"
.to_string(),
),
manifest_signed: false,
+2 -1
View File
@@ -885,8 +885,9 @@ mod tests {
assert!(!image_uses_insecure_registry(
"23.182.128.160:3000/lfg2025/filebrowser:v2.27.0"
));
// HTTPS registries never match the insecure list.
assert!(!image_uses_insecure_registry(
"git.tx1138.com/lfg2025/bitcoin-knots:latest"
"ghcr.io/lfg2025/bitcoin-knots:latest"
));
assert!(!image_uses_insecure_registry(
"docker.io/library/nginx:latest"