Compare commits

..
Author SHA1 Message Date
hermes-bot 14dc5247b7 fix(youtrack): OIDC client as an OIDCClient resource, not Ingress annotations
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Uses the OIDCClient CRD from #1535; the Ingress carries only routing and
DNS again. The Flux Kustomization treats OIDCClient as current on apply,
since its Ready reflects Authentik state (the group), as with KumaMonitor.
2026-11-01 09:31:45 +00:00
hermes-bot 68d1a174db fix(youtrack): OIDC client from authentik-gate-sync, no blueprint
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
The provider, application and group policy are now created by
authentik-gate-sync from the Ingress annotations auth.renner.uno/oidc-client
and auth.renner.uno/group (#1535); the youtrack.yaml blueprint is gone.
The group YouTrack Users stays managed in Authentik.
2026-11-01 09:22:40 +00:00
hermes-bot 7efc62e216 fix(youtrack): take the YouTrack Users group out of the blueprint
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was canceled
Groups are managed in Authentik itself. The blueprint now only binds the
application to the group by name (!Find).

The YouTrack entries move to their own blueprint (youtrack.yaml). Authentik
imports a blueprint atomically (importer.py apply() runs _apply_models in
one transaction), so while the group does not exist the whole YouTrack
blueprint rolls back: no provider, no application, login closed. In the
shared OAuth2 blueprint the same miss would also have blocked every other
provider.
2026-11-01 09:16:22 +00:00
hermes-bot f50b33cadf fix(youtrack): manage YouTrack Users members in Authentik, not the blueprint
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline failed
Same as Wiki Readers: the blueprint only ensures the group exists, because
the application's policy binding refers to it; membership is edited in
Authentik.
2026-11-01 08:45:40 +00:00
hermes-bot f96737c576 feat(youtrack): YouTrack Server, LAN only, login through Authentik
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was canceled
Namespace youtrack: jetbrains/youtrack 2026.2.19728 (digest-pinned), one
replica with Recreate, PVC youtrack (data/conf/logs/backups as subPaths),
Velero youtrack-daily plus restore-youtrack like the other apps.

The Ingress carries only dns.renner.uno/internal, so the UDM gets a record
and netbird-service-sync publishes nothing: the host is not reachable from
the internet until login goes through Authentik and a later PR adds the
public label.

Authentik blueprint: group YouTrack Users, confidential OIDC provider
(client_id youtrack; YouTrack's OpenID Connect auth module requires a
client secret), application slug youtrack, bound to the group.

YouTrack Server has no documented unattended first install (2026.2 docs:
the Configuration Wizard is mandatory, the skip flag exists for upgrades
only), so the wizard and the auth module are a one-time owner step,
accepted by the owner.
2026-11-01 08:42:43 +00:00
25 changed files with 364 additions and 76 deletions
+4 -4
View File
@@ -35,25 +35,25 @@ when:
steps:
- name: model-selection-tests
depends_on: []
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.312
commands:
- sh scripts/ci/test-model-selection.sh
- name: pr-review-mistral-tests
depends_on: []
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.312
commands:
- sh scripts/ci/test-pr-review-mistral.sh
- name: promtool-rules-guard-tests
depends_on: []
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.312
commands:
- sh scripts/ci/test-promtool-rules.sh
- name: yaml-citations-guard-tests
depends_on: []
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.312
commands:
- sh scripts/ci/test-yaml-citations.sh
+1 -1
View File
@@ -26,7 +26,7 @@ steps:
# fetch-models.sh response mapping. No cluster, no secrets, no network — the
# ci-claude image just provides sh + jq. Keeps the flag-injection regression
# test from rotting (it exercises the exact injection shapes).
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.312
commands:
- sh scripts/ci/test-model-selection.sh
+1 -1
View File
@@ -70,7 +70,7 @@ steps:
# this step is skipped — otherwise we'd fork-bomb PRs.
when:
- branch: main
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.312
environment:
CLAUDE_CODE_OAUTH_TOKEN:
from_secret: claude_code_oauth_token
+1 -1
View File
@@ -30,7 +30,7 @@ when:
steps:
- name: update-audit-agent
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.312
environment:
CLAUDE_CODE_OAUTH_TOKEN:
from_secret: claude_code_oauth_token
-7
View File
@@ -22,13 +22,6 @@ Two rules, one per family, send every packet with a pod source to table 47
(`talos/patches/site-homelab.yaml`, priorities 1000/1001, below main). The
table decides by longest prefix; no rule carves out exceptions.
A reply from a LoadBalancer VIP is pod traffic as well, but it reaches the
routing decision with the VIP as its source. Two more rules (1002/1003) send
the LoadBalancer pool to table 47, so the reply leaves over `ingress0`, where
the request arrived as the BGP next hop. Without them main sent it out
`mgmt0`. The pool is a site fact: the site patch's `lb-pool-site` ConfigMap,
which `bgp-pool` takes by Flux substitution, and the rules beside it.
bird (`kubernetes/infrastructure/bird/`) owns table 47 and fills it with:
- the node's attached networks — `mgmt0`, `ingress0`, `pod0`, `node0` —
+1 -1
View File
@@ -105,7 +105,7 @@ spec:
type: RuntimeDefault
containers:
- name: controller
image: gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-11-01-1059 # {"$imagepolicy": "flux-system:authentik-gate-sync"}
image: gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-29-1349 # {"$imagepolicy": "flux-system:authentik-gate-sync"}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
@@ -64,7 +64,7 @@ spec:
type: RuntimeDefault
containers:
- name: shelly-manager
image: gitea.renner.uno/konairius/shelly-manager:2026-11-02-0659 # {"$imagepolicy": "flux-system:shelly-manager"}
image: gitea.renner.uno/konairius/shelly-manager:2026-10-30-1343 # {"$imagepolicy": "flux-system:shelly-manager"}
imagePullPolicy: IfNotPresent
ports:
- name: http
+90
View File
@@ -0,0 +1,90 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: youtrack
namespace: youtrack
labels:
app.kubernetes.io/name: youtrack
spec:
replicas: 1
# One process owns the database directory.
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: youtrack
template:
metadata:
labels:
app.kubernetes.io/name: youtrack
spec:
automountServiceAccountToken: false
enableServiceLinks: false
securityContext:
runAsNonRoot: true
# The image's service account.
runAsUser: 13001
runAsGroup: 13001
fsGroup: 13001
seccompProfile:
type: RuntimeDefault
containers:
- name: youtrack
image: jetbrains/youtrack:2026.2.19728@sha256:c5e9cedc6fb1d7bcc9b3b56fc3f63e563623e1cd39396161eb70957991583950
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
ports:
- name: http
containerPort: 8080
resources:
requests:
cpu: 250m
memory: 2Gi
limits:
memory: 4Gi
startupProbe:
tcpSocket:
port: http
periodSeconds: 10
failureThreshold: 60
readinessProbe:
tcpSocket:
port: http
periodSeconds: 10
livenessProbe:
tcpSocket:
port: http
periodSeconds: 30
failureThreshold: 4
volumeMounts:
- name: youtrack
mountPath: /opt/youtrack/data
subPath: data
- name: youtrack
mountPath: /opt/youtrack/conf
subPath: conf
- name: youtrack
mountPath: /opt/youtrack/logs
subPath: logs
- name: youtrack
mountPath: /opt/youtrack/backups
subPath: backups
volumes:
- name: youtrack
persistentVolumeClaim:
claimName: youtrack
---
apiVersion: v1
kind: Service
metadata:
name: youtrack
namespace: youtrack
spec:
selector:
app.kubernetes.io/name: youtrack
ports:
- name: http
port: 8080
targetPort: http
+27
View File
@@ -0,0 +1,27 @@
# LAN-only (UDM record via dns.renner.uno/internal); no public label until login goes through Authentik.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: youtrack
namespace: youtrack
labels:
dns.renner.uno/internal: "true"
annotations:
cert-manager.io/cluster-issuer: letsencrypt-cluster
spec:
ingressClassName: traefik
tls:
- hosts:
- youtrack.renner.uno
secretName: youtrack-tls
rules:
- host: youtrack.renner.uno
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: youtrack
port:
number: 8080
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- pvc.yaml
- deployment.yaml
- ingress.yaml
- oidcclient.yaml
+13
View File
@@ -0,0 +1,13 @@
# Authentik OIDC client, created by authentik-gate-sync; issuer https://auth.renner.uno/application/o/youtrack/.
apiVersion: auth.renner.uno/v1alpha1
kind: OIDCClient
metadata:
name: youtrack
namespace: youtrack
spec:
launchURL: https://youtrack.renner.uno
group: YouTrack Users
redirectURIs:
# YouTrack generates the callback when the auth module is created; pin it strict once known.
- matchingMode: regex
url: ^https://youtrack\.renner\.uno/.*$
+17
View File
@@ -0,0 +1,17 @@
# YouTrack data, conf, logs and backups (subPaths); Velero youtrack-daily.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: youtrack
namespace: youtrack
labels:
app.kubernetes.io/name: youtrack
annotations:
kustomize.toolkit.fluxcd.io/prune: disabled
spec:
accessModes:
- ReadWriteOnce
storageClassName: ceph-block
resources:
requests:
storage: 20Gi
@@ -89,9 +89,6 @@ spec:
substituteFrom:
- kind: ConfigMap
name: cluster-settings
# The LoadBalancer pool, from the site's Talos patch (site-*.yaml).
- kind: ConfigMap
name: lb-pool-site
dependsOn:
- name: infrastructure
---
+28
View File
@@ -394,3 +394,31 @@ spec:
status.validationErrors.exists(e, e.startsWith('No backups found for schedule')))))
current: >-
has(status.phase) && (status.phase == 'Completed' || status.phase == 'FailedValidation')
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: restore-youtrack
namespace: flux-system
spec:
interval: 10m
timeout: 1h
sourceRef:
kind: GitRepository
name: flux-system
path: ./kubernetes/restore/youtrack
prune: true
wait: true
dependsOn:
- name: restore-gitea
- name: velero-backups-synced
- name: infrastructure-ceph-cluster
healthCheckExprs:
- apiVersion: velero.io/v1
kind: Restore
failed: >-
has(status.phase) && (status.phase in ['Failed', 'PartiallyFailed'] ||
(status.phase == 'FailedValidation' && !(has(status.validationErrors) &&
status.validationErrors.exists(e, e.startsWith('No backups found for schedule')))))
current: >-
has(status.phase) && (status.phase == 'Completed' || status.phase == 'FailedValidation')
+25
View File
@@ -0,0 +1,25 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: youtrack
namespace: flux-system
spec:
interval: 10m
retryInterval: 1m
timeout: 15m
sourceRef:
kind: GitRepository
name: flux-system
path: ./kubernetes/apps/youtrack
prune: true
wait: true
# OIDCClient Ready tracks Authentik (e.g. the group exists), not this apply.
healthCheckExprs:
- apiVersion: auth.renner.uno/v1alpha1
kind: OIDCClient
current: "true"
dependsOn:
- name: restore-youtrack
- name: infrastructure
- name: infrastructure-crds
- name: infrastructure-ceph-cluster
@@ -83,6 +83,11 @@ metadata:
---
apiVersion: v1
kind: Namespace
metadata:
name: youtrack
---
apiVersion: v1
kind: Namespace
metadata:
name: immich
---
@@ -32,6 +32,7 @@ resources:
- wordpress-ns.yaml
- ai-ns.yaml
- navidrome-ns.yaml
- youtrack-ns.yaml
- uptime-monitor-ns.yaml
- kuma-sync-ns.yaml
- cloudflare-lb-sync-ns.yaml
@@ -0,0 +1,107 @@
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: default-deny-ingress
namespace: youtrack
spec:
endpointSelector: {}
ingress:
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": youtrack
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": ingress
app.kubernetes.io/name: traefik
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": monitoring
- fromEntities:
- host
- remote-node
# ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See
# home-assistant-ns.yaml for the rationale (CT-RELATED path not
# passing related ICMP empirically; per-namespace shape, mirroring
# commit 3c517a0's verified-safe pattern with the 66dbe9f type set).
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
operator: Exists
icmps:
- fields:
- { type: 0, family: IPv4 }
- { type: 3, family: IPv4 }
- { type: 8, family: IPv4 }
- { type: 11, family: IPv4 }
- { type: 12, family: IPv4 }
- { type: 1, family: IPv6 }
- { type: 2, family: IPv6 }
- { type: 3, family: IPv6 }
- { type: 4, family: IPv6 }
- { type: 128, family: IPv6 }
- { type: 129, family: IPv6 }
- { type: 133, family: IPv6 }
- { type: 134, family: IPv6 }
- { type: 135, family: IPv6 }
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
egress:
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": youtrack
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": ingress
app.kubernetes.io/name: traefik
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
# host/remote-node: kubelet probes + node-local reach (Ceph mons on host net)
- toEntities:
- host
- remote-node
# ICMP — egress mirror; see ingress block above.
- toEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
operator: Exists
icmps:
- fields:
- { type: 0, family: IPv4 }
- { type: 3, family: IPv4 }
- { type: 8, family: IPv4 }
- { type: 11, family: IPv4 }
- { type: 12, family: IPv4 }
- { type: 1, family: IPv6 }
- { type: 2, family: IPv6 }
- { type: 3, family: IPv6 }
- { type: 4, family: IPv6 }
- { type: 128, family: IPv6 }
- { type: 129, family: IPv6 }
- { type: 133, family: IPv6 }
- { type: 134, family: IPv6 }
- { type: 135, family: IPv6 }
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
---
# Outbound HTTPS: OIDC discovery/token/JWKS at auth.renner.uno (Traefik VIP, matched as traefik
# pods above) and JetBrains services (license, update check), mail-free.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: youtrack-egress-https
namespace: youtrack
spec:
endpointSelector: {}
egress:
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
@@ -8,10 +8,6 @@
# Selector is opt-in via `lb-pool: bgp`. There is no default pool —
# Services without this label do not get a LoadBalancer IP.
#
# The ranges are a site fact: lb-pool-site from the site's Talos patch
# (talos/patches/site-*.yaml), by Flux substitution. A CIDR block uses all its
# addresses (allowFirstLastIPs defaults to Yes).
#
# IPv4 + IPv6 ranges keep matching last bytes (10.2.99.x ↔ fd99::x)
# so each LB has the same "tail" in both families.
apiVersion: cilium.io/v2alpha1
@@ -23,8 +19,10 @@ spec:
matchLabels:
lb-pool: bgp
blocks:
- cidr: "${LB_POOL_V4}"
- cidr: "${LB_POOL_V6}"
- start: "10.2.99.0"
stop: "10.2.99.255"
- start: "fd99::"
stop: "fd99::ff"
---
# Cilium BGPv2 control-plane config peering each node with the UDM over the
# ingress VLAN 47 — never over v30iot, which is the untrusted IoT segment.
@@ -369,3 +369,23 @@ spec:
labelSelector:
matchLabels:
app.kubernetes.io/name: synapse
---
apiVersion: velero.io/v1
kind: Schedule
metadata:
name: youtrack-daily
namespace: velero
labels:
app.kubernetes.io/part-of: homelab-backup
spec:
# Daily at 10:15 local, next free stagger slot after matrix-daily at 09:45.
schedule: "15 10 * * *"
useOwnerReferencesInBackup: false
template:
includedNamespaces:
- youtrack
includedResources:
- persistentvolumeclaims
storageLocation: default
ttl: 720h0m0s
snapshotMoveData: true
@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- restore.yaml
+7
View File
@@ -0,0 +1,7 @@
apiVersion: velero.io/v1
kind: Restore
metadata:
name: youtrack
namespace: velero
spec:
scheduleName: youtrack-daily
-12
View File
@@ -67,18 +67,6 @@ cluster:
nameservers:
- 185.12.64.1
- 185.12.64.2
# The LoadBalancer pool (see site-homelab.yaml). Services pin their
# addresses from the production pool, so a test run carries the same one.
- name: lb-pool-site
contents: |
apiVersion: v1
kind: ConfigMap
metadata:
name: lb-pool-site
namespace: flux-system
data:
LB_POOL_V4: 10.2.99.0/24
LB_POOL_V6: fd99::/120
---
# Host DNS asks k8s-gateway first, so containerd resolves in-cluster names
# (gitea.renner.uno) in the cluster; bootstrap.sh installs k8s-gateway right
@@ -85,18 +85,6 @@ cluster:
nameservers:
- fd02:47::1
- 10.2.47.1
# The LoadBalancer pool (bgp-pool, infrastructure/post-crds/cilium-bgp.yaml,
# by Flux substitution). The routing rules 1002/1003 below name it too.
- name: lb-pool-site
contents: |
apiVersion: v1
kind: ConfigMap
metadata:
name: lb-pool-site
namespace: flux-system
data:
LB_POOL_V4: 10.2.99.0/24
LB_POOL_V6: fd99::/120
---
# All pod traffic goes to table 47, and the table decides — longest prefix
# match, no exceptions to carve out. bird fills it with the node's attached
@@ -127,21 +115,6 @@ name: "1001"
src: 10.244.0.0/16
table: "47"
---
# A LoadBalancer reply is pod traffic too, but it reaches the routing decision
# with the VIP as its source, so the rules above miss it and main sends it out
# mgmt0 while the request came in over ingress0. The pool is lb-pool-site above.
apiVersion: v1alpha1
kind: RoutingRuleConfig
name: "1002"
src: fd99::/120
table: "47"
---
apiVersion: v1alpha1
kind: RoutingRuleConfig
name: "1003"
src: 10.2.99.0/24
table: "47"
---
# Host DNS asks k8s-gateway first, so containerd resolves in-cluster names
# (gitea.renner.uno) in the cluster; bootstrap.sh installs k8s-gateway right
# after the CNI, before anything else needs DNS. The UDM is the site's
-12
View File
@@ -47,15 +47,3 @@ cluster:
values.yaml: |
deployment:
dnsPolicy: Default
# The LoadBalancer pool (see site-homelab.yaml). Services pin their
# addresses from the production pool, so a test run carries the same one.
- name: lb-pool-site
contents: |
apiVersion: v1
kind: ConfigMap
metadata:
name: lb-pool-site
namespace: flux-system
data:
LB_POOL_V4: 10.2.99.0/24
LB_POOL_V6: fd99::/120