infra/stacks/anisette/main.tf
Viktor Barzin 96addf65b4
Some checks failed
ci/woodpecker/push/default Pipeline was canceled
fix(anisette): docker.io/ image prefix to pass Kyverno require-trusted-registries
First apply was denied at admission — a bare dadoum/anisette-v3-server@sha256
ref isn't in the trusted-registries allowlist (only enumerated DockerHub
user-repo prefixes are). docker.io/* IS allowlisted, so use the explicit
registry prefix; still pulls via the 10.0.20.10 pull-through cache.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:47:05 +00:00

179 lines
6 KiB
HCL

# 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).
# The `docker.io/` prefix is REQUIRED, not cosmetic: the Kyverno
# require-trusted-registries policy allowlists `docker.io/*` but NOT a
# bare `dadoum/*` prefix (only enumerated DockerHub user repos like
# mendhak/*, mpepping/* are listed in
# stacks/kyverno/modules/kyverno/security-policies.tf). A bare
# `dadoum/anisette-v3-server@...` is denied at admission; the explicit
# docker.io/ registry matches the allowlist and still pulls via the
# 10.0.20.10 pull-through cache.
image = "docker.io/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"
}
}