diff --git a/.claude/reference/service-catalog.md b/.claude/reference/service-catalog.md index ec78beac..242d1189 100644 --- a/.claude/reference/service-catalog.md +++ b/.claude/reference/service-catalog.md @@ -42,6 +42,7 @@ | webhook_handler | Webhook processing | webhook_handler | | tuya-bridge | Smart home bridge | tuya-bridge | | android-emulator | Shared Android 16 test emulator (adb 10.0.20.200:5555, noVNC android-emulator.viktorbarzin.lan) | android-emulator | +| anisette | Self-hosted Apple anisette-data server (Dadoum/anisette-v3-server, digest-pinned) for sideloading the TripIt iOS Shell via SideStore; internal-only http://anisette.viktorbarzin.lan, auth=none, LAN-only, stateless | anisette | | dawarich | Location history | dawarich | | owntracks | Location tracking | owntracks | | nextcloud | File sync/share | nextcloud | diff --git a/stacks/anisette/main.tf b/stacks/anisette/main.tf new file mode 100644 index 00000000..a8fbb8ec --- /dev/null +++ b/stacks/anisette/main.tf @@ -0,0 +1,171 @@ +# anisette — self-hosted Apple anisette-data server for SideStore/AltStore. +# +# Purpose (infra issue #40): the TripIt iOS Shell is sideloaded with SideStore +# using a free Apple ID. SideStore needs an "anisette" server to broker the +# Apple-ID auth dance; the public community anisette servers see every login, +# so we run our own. Stateless HTTP service on a stable INTERNAL endpoint +# (anisette.viktorbarzin.lan) that SideStore points at. +# +# Image: Dadoum/anisette-v3-server — the de-facto standard anisette-v3 server +# for SideStore/AltStore (the same project SideStore's own docs point at). +# Upstream publishes ONLY a mutable :latest tag (no GitHub releases, no semver, +# no date/sha tags — verified 2026-06-14), so we pin by MANIFEST DIGEST instead +# (immutable, honours the "never :latest" rule). DockerHub is pulled +# transparently via the registry-VM pull-through cache, same as echo/cyberchef. +# To bump: `docker buildx imagetools inspect dadoum/anisette-v3-server:latest`, +# then replace the digest below. +# +# Stateless: the container caches Apple provisioning libraries under +# /home/Alcoholic/.config/anisette-v3/lib (a regenerable download — re-fetched +# if absent — and per upstream issue #23 it does NOT preserve client auth across +# restarts anyway). So an emptyDir is the honest fit: keeps that path writable +# without taking on a backup-pipeline obligation. No PVC, no Vault secret. + +variable "tls_secret_name" { + type = string + sensitive = true +} + +resource "kubernetes_namespace" "anisette" { + metadata { + name = "anisette" + labels = { + "istio-injection" : "disabled" + tier = local.tiers.aux + } + } + lifecycle { + # KYVERNO_LIFECYCLE_V1: goldilocks-vpa-auto-mode ClusterPolicy stamps this label on every namespace + ignore_changes = [metadata[0].labels["goldilocks.fairwinds.com/vpa-update-mode"]] + } +} + +module "tls_secret" { + source = "../../modules/kubernetes/setup_tls_secret" + namespace = kubernetes_namespace.anisette.metadata[0].name + tls_secret_name = var.tls_secret_name +} + +resource "kubernetes_deployment" "anisette" { + metadata { + name = "anisette" + namespace = kubernetes_namespace.anisette.metadata[0].name + labels = { + app = "anisette" + tier = local.tiers.aux + } + } + spec { + replicas = 1 + selector { + match_labels = { + app = "anisette" + } + } + template { + metadata { + labels = { + app = "anisette" + } + annotations = { + # Diun notify-only watch. Upstream tags only :latest, so watch the + # digest of :latest rather than a semver pattern. + "diun.enable" = "true" + "diun.watch_repo" = "false" + "diun.include_tags" = "^latest$" + } + } + spec { + container { + # Pinned by digest — upstream ships only a mutable :latest (no tags). + image = "dadoum/anisette-v3-server@sha256:1e20384985d3c49965f444bef39d627768dacc39ea0dca91f2a535edb7591ba3" + name = "anisette" + port { + name = "http" + container_port = 6969 + } + # The image runs as the non-root user "Alcoholic" and writes its + # provisioning-library cache here; back it with an emptyDir so the + # path is writable (stateless — wiped on restart, re-downloaded). + volume_mount { + name = "provisioning-cache" + mount_path = "/home/Alcoholic/.config/anisette-v3/lib" + } + resources { + requests = { + cpu = "10m" + memory = "128Mi" + } + limits = { + memory = "128Mi" + } + } + readiness_probe { + http_get { + path = "/" + port = 6969 + } + period_seconds = 15 + initial_delay_seconds = 5 + } + liveness_probe { + http_get { + path = "/" + port = 6969 + } + period_seconds = 30 + failure_threshold = 6 + } + } + volume { + name = "provisioning-cache" + empty_dir {} + } + } + } + } + lifecycle { + ignore_changes = [ + spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1 + ] + } +} + +resource "kubernetes_service" "anisette" { + metadata { + name = "anisette" + namespace = kubernetes_namespace.anisette.metadata[0].name + labels = { + "app" = "anisette" + } + } + spec { + selector = { + app = "anisette" + } + port { + name = "http" + port = "80" + target_port = "6969" + } + } +} + +module "ingress" { + source = "../../modules/kubernetes/ingress_factory" + # auth = "none": SideStore is a native iOS client — it can't replay the + # Authentik forward-auth cookie dance, so Authentik would break it (same + # reasoning as android-emulator's adb). Internal-only: anisette.viktorbarzin.lan, + # allow_local_access_only locks it to the LAN, and it brokers no user data of + # ours (it just relays Apple-ID anisette data). Never publicly exposed. + auth = "none" + namespace = kubernetes_namespace.anisette.metadata[0].name + name = "anisette" + root_domain = "viktorbarzin.lan" + tls_secret_name = var.tls_secret_name + allow_local_access_only = true + ssl_redirect = false + extra_annotations = { + "gethomepage.dev/enabled" = "false" + } +} diff --git a/stacks/anisette/secrets b/stacks/anisette/secrets new file mode 120000 index 00000000..ca54a7cf --- /dev/null +++ b/stacks/anisette/secrets @@ -0,0 +1 @@ +../../secrets \ No newline at end of file diff --git a/stacks/anisette/terragrunt.hcl b/stacks/anisette/terragrunt.hcl new file mode 100644 index 00000000..0d1c8e53 --- /dev/null +++ b/stacks/anisette/terragrunt.hcl @@ -0,0 +1,8 @@ +include "root" { + path = find_in_parent_folders() +} + +dependency "platform" { + config_path = "../platform" + skip_outputs = true +}