Compare commits

..

1 Commits

Author SHA1 Message Date
unkin-agent 3fa12e4e5a Deliver the cert helpers to the puppet compiler pods
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
certmanager and sshsignhost are invoked server-side by generate() during catalog compilation but only exist on the legacy VM masters, so compiles on the k8s compilers fail with "No such file or directory".

- Stage a self-contained EL9 python runtime on the shared bins volume
- Render both helpers from their puppet-prod ERB templates on the code volume rather than copying the scripts here
- Mount a name-dispatching launcher at /usr/local/bin/{certmanager,sshsignhost}
- Mount kubernetes-auth configs at /opt/{certmanager,sshsignhost}/config.yaml

Needs terraform-vault #152 applied and the puppet-prod kubernetes-auth PR merged.
2026-09-13 23:26:34 +10:00
145 changed files with 3426 additions and 1988 deletions
+29
View File
@@ -0,0 +1,29 @@
when:
- event: pull_request
steps:
- name: vector-test
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/timberio/vector:0.57.0-debian
commands:
# Dummy creds + writable dirs so the full topologies build; the unit tests
# only exercise the transforms (sources are not started).
- export CLICKHOUSE_USER=ci CLICKHOUSE_PASSWORD=ci
- export NATS_PRODUCER_PASSWORD=ci NATS_CONSUMER_PASSWORD=ci
- mkdir -p /vector-data-dir /etc/vault-ca
- cp /etc/ssl/certs/ca-certificates.crt /etc/vault-ca/ca.crt
# Transform tier + VM ingest: unit-tested transforms.
- vector test apps/base/logging/vector/aggregator.yaml apps/base/logging/vector/aggregator-tests.yaml
- vector test apps/base/logging/vector/vm-ingest.yaml apps/base/logging/vector/vm-ingest-tests.yaml
# Agent has no transforms to unit-test; validate it builds. (The archiver
# leg is now the logarchiver service, not a Vector pipeline.)
- vector validate --no-environment apps/base/logging/vector/agent.yaml
backend_options:
kubernetes:
serviceAccountName: default
resources:
requests:
memory: 256Mi
cpu: 250m
limits:
memory: 1Gi
cpu: 1
+1 -1
View File
@@ -36,7 +36,7 @@ spec:
mountPath: /combined-certs
containers:
- name: api
image: git.unkin.net/unkin/artifactapi:v3.12.0
image: git.unkin.net/unkin/artifactapi:v3.11.2
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8000
+1 -1
View File
@@ -22,7 +22,7 @@ spec:
automountServiceAccountToken: true
containers:
- name: ui
image: git.unkin.net/unkin/artifactapi-ui:v3.12.0
image: git.unkin.net/unkin/artifactapi-ui:v3.11.2
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
+6 -21
View File
@@ -64,12 +64,8 @@ spec:
archive_mode: "on"
archive_timeout: 5min
dynamic_shared_memory_type: posix
effective_cache_size: 1536MB
effective_cache_size: 256MB
full_page_writes: "on"
# Replicas report their oldest xmin to the primary, so multi-second reads on
# a hot standby stop exhausting max_standby_streaming_delay and being
# cancelled. Retained-dead-tuple cost is negligible on a ~155MB database.
hot_standby_feedback: "on"
log_destination: csvlog
log_directory: /controller/log
log_filename: postgres
@@ -81,12 +77,7 @@ spec:
max_parallel_workers: "16"
max_replication_slots: "16"
max_worker_processes: "16"
# A pg_stat_statements.* parameter is what makes CNPG treat the extension as
# managed and run CREATE EXTENSION in every database; preloading alone does
# not create it.
pg_stat_statements.max: "10000"
pg_stat_statements.track: top
shared_buffers: 512MB
shared_buffers: 128MB
shared_memory_type: mmap
ssl_max_protocol_version: TLSv1.3
ssl_min_protocol_version: TLSv1.3
@@ -95,9 +86,6 @@ spec:
wal_log_hints: "on"
wal_receiver_timeout: 5s
wal_sender_timeout: 5s
# CNPG merges this with the libraries it manages itself.
shared_preload_libraries:
- pg_stat_statements
syncReplicaElectionConstraint:
enabled: false
primaryUpdateMethod: restart
@@ -117,16 +105,13 @@ spec:
updateInterval: 30
resources:
limits:
# 500m is a 50ms CFS quota per 100ms period, exhausted by bursts even at
# ~0.01 cores average, so every query pays throttle latency.
cpu: "2"
cpu: 500m
# 512Mi OOMKilled replicas under load (shared_buffers 128MB +
# max_connections 200 leave no headroom) — see incident 2026-07-28.
# shared_buffers 512MB needs the same headroom multiple, hence 2Gi.
memory: 2Gi
requests:
cpu: 500m
memory: 1Gi
requests:
cpu: 50m
memory: 512Mi
smartShutdownTimeout: 180
startDelay: 3600
stopDelay: 1800
-32
View File
@@ -37,22 +37,6 @@ spec:
name: authentik
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: authentik-server
port: 80
weight: 1
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplaceFullPath
replaceFullPath: /application/o/token/
matches:
- path:
type: Exact
value: /application/o/token
- backendRefs:
- group: ""
kind: Service
@@ -102,22 +86,6 @@ spec:
name: authentik-internal
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: authentik-server
port: 80
weight: 1
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplaceFullPath
replaceFullPath: /application/o/token/
matches:
- path:
type: Exact
value: /application/o/token
- backendRefs:
- group: ""
kind: Service
-1
View File
@@ -19,7 +19,6 @@ resources:
- redis-deployment.yaml
- redis-pvc.yaml
- redis-service.yaml
- server-vmpodscrape.yaml
- vaultauth.yaml
- vaultstaticsecret.yaml
- vmpodscrape.yaml
@@ -1,16 +0,0 @@
---
# Scrape the authentik server's django_prometheus endpoint (:9300). Picked up
# by the observability VMAgent (selectAllByDefault).
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: authentik-server
namespace: authentik
spec:
selector:
matchLabels:
app.kubernetes.io/name: authentik
app.kubernetes.io/component: server
podMetricsEndpoints:
- port: metrics
path: /metrics
@@ -1,6 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ns1.yaml
@@ -1,16 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: acme-ns1-a
namespace: bind-external
spec:
zoneRef: acme-unkin-net
name: ns1
type: A
ttl: 3600
values:
# Public address of this cluster's external BIND, same target as
# acme-ns1.unkin.net. Resolvers that cached the seeded ns1.acme.unkin.net
# NS name must still reach the zone.
- 103.216.191.185
@@ -1,11 +0,0 @@
---
# Authoritative delegation records for acme.unkin.net. Without these the zone
# only holds the operator's seed apex (NS ns1.acme.unkin.net glued to the
# primary pod IP), which is unroutable off-cluster and goes stale on
# reschedule. DNSRecords must live in the same namespace as their BindZone.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ns
- a
@@ -1,16 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: acme-apex-ns
namespace: bind-external
spec:
zoneRef: acme-unkin-net
# "@" is the zone apex.
name: "@"
type: NS
ttl: 3600
values:
# Matches the parent delegation in Google Cloud DNS. Out of zone, so the
# child needs no glue of its own.
- acme-ns1.unkin.net.
@@ -7,5 +7,4 @@ resources:
- cluster.yaml
- tsigkey.yaml
- zones.yaml
- acme-unkin-net
- agent-dns-rolebinding.yaml
-11
View File
@@ -17,14 +17,3 @@ spec:
updateKeyRef: certmanager
allowTransfer:
- key certmanager
# Published apex NS. acme-ns1 is what the parent delegates to and glues; ns1 is
# in-zone, so its address is declared below or a reseed would glue it to the
# primary pod IP.
nameservers:
- acme-ns1.unkin.net.
- ns1.acme.unkin.net.
records:
- name: ns1
type: A
ttl: 3600
values: ["103.216.191.185"]
@@ -1,15 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: dashboard-ceph-cname
namespace: bind-internal
spec:
zoneRef: ceph-unkin-net
name: dashboard
type: CNAME
ttl: 600
values:
# Ceph mgr dashboard, reached via lb1. Lets in-cluster clients (the
# cephrgw-operator) resolve dashboard.ceph.unkin.net.
- lb1.unkin.net.
@@ -1,7 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- dashboard.yaml
- s3.yaml
@@ -1,15 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: s3-ceph-cname
namespace: bind-internal
spec:
zoneRef: ceph-unkin-net
name: s3
type: CNAME
ttl: 600
values:
# radosgw S3 endpoint. Points at the Consul service for now; the real
# target will be changed later.
- radosgw.service.consul.
@@ -1,6 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- cname
@@ -2,15 +2,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Individually-managed authoritative records live under <zone>/<type>/<record>.yaml.
# DNSRecords must live in the same namespace as their BindZone (the operator
# resolves zoneRef/clusterRef/updateKeyRef within the record's namespace), so
# these sit alongside the zone in bind-internal, not in the app namespace.
resources:
- cluster.yaml
- tsigkey.yaml
- zones.yaml
- unkin-net
- main-unkin-net
- ceph-unkin-net
- records.yaml
- acls.yaml
@@ -1,14 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: jellyfin-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: jellyfin
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -1,12 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- jellyfin.yaml
- lidarr.yaml
- nzbget.yaml
- prowlarr.yaml
- radarr.yaml
- readarr.yaml
- sonarr.yaml
@@ -1,14 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: lidarr-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: lidarr
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -1,14 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: nzbget-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: nzbget
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -1,14 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: prowlarr-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: prowlarr
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -1,14 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: radarr-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: radarr
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -1,14 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: readarr-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: readarr
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -1,14 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: sonarr-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: sonarr
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -1,6 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- a
@@ -0,0 +1,164 @@
# Individually-managed authoritative records for the unkin.net zone.
# DNSRecords must live in the same namespace as their BindZone (the operator
# resolves zoneRef/clusterRef/updateKeyRef within the record's namespace), so
# these sit alongside the zone in bind-internal, not in the app namespace.
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
# "internal" in the name distinguishes this from the external DNS that
# Authentik will manage its own records from later.
name: identity-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: identity
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the authentik Gateway serves the
# identity.unkin.net hostname there.
- 198.18.199.0
---
# PRODUCTION CUTOVER RECORD — intentionally commented out.
# git.unkin.net currently resolves to the LIVE VM forge (HAProxy VRRP VIP
# 198.18.19.17), which holds every repo the estate depends on. Uncommenting this
# repoints the whole org's git.unkin.net at the new k8s Gitea gateway VIP, so it
# is the FINAL step of the forge migration — gated on the data migration (gitea
# dump/restore + SECRET_KEY copy) in argocd-apps docs/gitea-migration.md.
# NOTE: the live git.unkin.net answer is served by the puppet DNS master today
# (profiles::dns::master, records from PuppetDB); this k8s apex zone holds only
# SOA+NS + a few DNSRecords so far. Confirm the k8s bind cluster is the live
# authority for unkin.net (or update the puppet record instead) before relying
# on this CR at cutover.
# ---
# apiVersion: bind.unkin.net/v1alpha1
# kind: DNSRecord
# metadata:
# name: git-dns-internal
# namespace: bind-internal
# spec:
# zoneRef: unkin-net
# name: git
# type: A
# ttl: 600
# values:
# # traefik-internal gateway VIP; the gitea Gateway serves git.unkin.net there.
# - 198.18.200.4
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: s3-ceph-cname
namespace: bind-internal
spec:
zoneRef: ceph-unkin-net
name: s3
type: CNAME
ttl: 600
values:
# radosgw S3 endpoint. Points at the Consul service for now; the real
# target will be changed later.
- radosgw.service.consul.
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: dashboard-ceph-cname
namespace: bind-internal
spec:
zoneRef: ceph-unkin-net
name: dashboard
type: CNAME
ttl: 600
values:
# Ceph mgr dashboard, reached via lb1. Lets in-cluster clients (the
# cephrgw-operator) resolve dashboard.ceph.unkin.net.
- lb1.unkin.net.
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: lb1-unkin-net
namespace: bind-internal
spec:
zoneRef: unkin-net
name: lb1
type: A
ttl: 600
values:
- 103.216.191.185
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: ghp-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: ghp
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the ghp Gateway serves ghp.unkin.net there.
- 198.18.200.4
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: arrstack-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: arrstack
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the arrproxy Gateway serves the
# arrstack.unkin.net front door (oauth2-proxy) there.
- 198.18.199.0
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: logviewer-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: logviewer
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the logviewer Gateway serves
# logviewer.unkin.net there.
- 198.18.200.4
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: cheeztv-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: cheeztv
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the cheeztv Gateway serves cheeztv.unkin.net
# there.
- 198.18.200.4
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: watchstate-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: watchstate
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the watchstate-external Gateway serves
# the watchstate.unkin.net front door (oauth2-proxy) there.
- 198.18.199.0
@@ -1,15 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: arrstack-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: arrstack
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the arrproxy Gateway serves the
# arrstack.unkin.net front door (oauth2-proxy) there.
- 198.18.199.0
@@ -1,14 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: auth-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: auth
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the kanidm Gateway serves auth.unkin.net there.
- 198.18.200.4
@@ -1,15 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: cheeztv-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: cheeztv
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the cheeztv Gateway serves cheeztv.unkin.net
# there.
- 198.18.200.4
@@ -1,14 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: fafflix-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: fafflix
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -1,14 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: ghp-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: ghp
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the ghp Gateway serves ghp.unkin.net there.
- 198.18.200.4
@@ -1,14 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: git-edge-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: git
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -1,23 +0,0 @@
---
# PRODUCTION CUTOVER RECORD — intentionally commented out.
# git.unkin.net currently resolves to the haproxy edge (git-edge.yaml), which
# forwards to the LIVE VM forge holding every repo. Uncommenting this
# repoints the whole org's git.unkin.net at the new k8s Gitea gateway VIP, so it
# is the FINAL step of the forge migration — gated on the data migration (gitea
# dump/restore + SECRET_KEY copy) in argocd-apps docs/gitea-migration.md.
# Uncomment this record AND its entry in kustomization.yaml (removing
# git-edge.yaml there) to activate it.
# ---
# apiVersion: bind.unkin.net/v1alpha1
# kind: DNSRecord
# metadata:
# name: git-dns-internal
# namespace: bind-internal
# spec:
# zoneRef: unkin-net
# name: git
# type: A
# ttl: 600
# values:
# # traefik-internal gateway VIP; the gitea Gateway serves git.unkin.net there.
# - 198.18.200.4
@@ -1,15 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: grafana-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: grafana
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the grafana-external Gateway serves
# grafana.unkin.net there.
- 198.18.199.0
@@ -1,17 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
# "internal" in the name distinguishes this from the external DNS that
# Authentik will manage its own records from later.
name: identity-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: identity
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the authentik Gateway serves the
# identity.unkin.net hostname there.
- 198.18.199.0
@@ -1,20 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- arrstack.yaml
- auth.yaml
- cheeztv.yaml
- fafflix.yaml
# PRODUCTION CUTOVER RECORD — see git.yaml. Uncomment together with the
# record itself, and remove git-edge.yaml.
# - git.yaml
- ghp.yaml
- git-edge.yaml
- grafana.yaml
- identity.yaml
- lb1.yaml
- logviewer.yaml
- vlogs.yaml
- watchstate.yaml
@@ -1,13 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: lb1-unkin-net
namespace: bind-internal
spec:
zoneRef: unkin-net
name: lb1
type: A
ttl: 600
values:
- 103.216.191.185
@@ -1,15 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: logviewer-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: logviewer
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the logviewer Gateway serves
# logviewer.unkin.net there.
- 198.18.200.4
@@ -1,15 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: vlogs-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: vlogs
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the vlogs-external Gateway serves the
# vlogs.unkin.net front door (oauth2-proxy) there.
- 198.18.199.0
@@ -1,15 +0,0 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: watchstate-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: watchstate
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the watchstate-external Gateway serves
# the watchstate.unkin.net front door (oauth2-proxy) there.
- 198.18.199.0
@@ -1,6 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- a
+1 -1
View File
@@ -21,7 +21,7 @@ spec:
runAsNonRoot: true
containers:
- name: operator
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/bind-operator:v0.3.1
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/bind-operator:v0.2.6
args:
- --metrics-bind-address=:8080
- --health-probe-bind-address=:8081
+1 -1
View File
@@ -6,7 +6,7 @@ resources:
- namespace.yaml
# CRDs are pulled from the bind-operator repo at the matching tag rather than
# vendored here, so they never drift from the operator.
- https://git.unkin.net/unkin/bind-operator/raw/tag/v0.3.1/config/crd/install.yaml
- https://git.unkin.net/unkin/bind-operator/raw/tag/v0.2.6/config/crd/install.yaml
- rbac.yaml
- agent-dns-rbac.yaml
- deployment.yaml
@@ -1,26 +0,0 @@
---
# Let's Encrypt *.ceph.unkin.net wildcard for the haproxy edge (ceph dashboard).
# DNS-01 needs the delegated _acme-challenge.ceph.unkin.net CNAME in the public
# unkin.net zone.
# _acme-challenge.ceph.unkin.net. CNAME _acme-challenge.ceph.acme.unkin.net.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-ceph-unkin-net
namespace: cert-manager
spec:
secretName: wildcard-ceph-unkin-net-tls
secretTemplate:
annotations:
reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "haproxy"
reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "haproxy"
privateKey:
size: 4096
dnsNames:
- "*.ceph.unkin.net"
issuerRef:
name: letsencrypt
kind: ClusterIssuer
group: cert-manager.io
@@ -1,26 +0,0 @@
---
# Let's Encrypt *.main.unkin.net wildcard for the haproxy edge (pve, arr stack,
# jellyfin, stalwart webadmin/autoconfig). DNS-01 needs the delegated
# _acme-challenge.main.unkin.net CNAME in the public unkin.net zone.
# _acme-challenge.main.unkin.net. CNAME _acme-challenge.main.acme.unkin.net.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-main-unkin-net
namespace: cert-manager
spec:
secretName: wildcard-main-unkin-net-tls
secretTemplate:
annotations:
reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "haproxy"
reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "haproxy"
privateKey:
size: 4096
dnsNames:
- "*.main.unkin.net"
issuerRef:
name: letsencrypt
kind: ClusterIssuer
group: cert-manager.io
@@ -14,9 +14,9 @@ spec:
secretTemplate:
annotations:
reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "cheeztv,arrstack,authentik,gitea,watchstate,mediamark,repospawner,haproxy,vlogs,grafana"
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "cheeztv,arrstack,authentik,gitea,watchstate,mediamark,repospawner"
reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "cheeztv,arrstack,authentik,gitea,watchstate,mediamark,repospawner,haproxy,vlogs,grafana"
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "cheeztv,arrstack,authentik,gitea,watchstate,mediamark,repospawner"
privateKey:
size: 4096
dnsNames:
@@ -12,5 +12,3 @@ resources:
- clusterissuer_letsencrypt.yaml
- clusterissuer_letsencrypt-staging.yaml
- certificate_wildcard-unkin-net.yaml
- certificate_wildcard-main-unkin-net.yaml
- certificate_wildcard-ceph-unkin-net.yaml
+1 -1
View File
@@ -26,7 +26,7 @@ data:
</key>
<value>
<PluginConfiguration>
<OidEndpoint>https://identity.unkin.net/application/o/jellyfin/</OidEndpoint>
<OidEndpoint>https://identity.k8s.syd1.au.unkin.net/application/o/jellyfin/</OidEndpoint>
<OidClientId>jellyfin</OidClientId>
<OidSecret>@@CLIENT_SECRET@@</OidSecret>
<Enabled>true</Enabled>
+2
View File
@@ -13,4 +13,6 @@ spec:
targetPort: http
selector:
app: cheeztv
# Pin each client to one replica to reduce transcode-session churn/takeover.
sessionAffinity: ClientIP
type: ClusterIP
+1 -3
View File
@@ -4,8 +4,6 @@ kind: StatefulSet
metadata:
name: cheeztv
namespace: cheeztv
annotations:
configmap.reloader.stakater.com/auto: "true"
spec:
# HA: two replicas coordinate transcode session ownership through Valkey and
# resume each other's HLS segments off the shared RWX transcode PVC. Stable
@@ -164,7 +162,7 @@ spec:
readOnly: true
containers:
- name: cheeztv
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/jellyfin-ha:v0.4.0
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/jellyfin-ha:v0.2.0
imagePullPolicy: IfNotPresent
ports:
- name: http
@@ -3,4 +3,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- apex.yaml
- namespace.yaml
@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Namespace
metadata:
labels:
app.kubernetes.io/name: clickhouse-system
name: clickhouse-system
+1 -1
View File
@@ -21,7 +21,7 @@ spec:
runAsNonRoot: true
containers:
- name: operator
image: git.unkin.net/unkin/kea-operator:v0.1.6
image: git.unkin.net/unkin/kea-operator:v0.1.5
args:
- --metrics-bind-address=:8080
- --health-probe-bind-address=:8081
+1 -1
View File
@@ -26,7 +26,7 @@ data:
</key>
<value>
<PluginConfiguration>
<OidEndpoint>https://identity.unkin.net/application/o/jellyfin/</OidEndpoint>
<OidEndpoint>https://identity.k8s.syd1.au.unkin.net/application/o/jellyfin/</OidEndpoint>
<OidClientId>jellyfin</OidClientId>
<OidSecret>@@CLIENT_SECRET@@</OidSecret>
<Enabled>true</Enabled>
+2
View File
@@ -13,4 +13,6 @@ spec:
targetPort: http
selector:
app: fafflix
# Pin each client to one replica to reduce transcode-session churn/takeover.
sessionAffinity: ClientIP
type: ClusterIP
+1 -3
View File
@@ -4,8 +4,6 @@ kind: StatefulSet
metadata:
name: fafflix
namespace: fafflix
annotations:
configmap.reloader.stakater.com/auto: "true"
spec:
# HA: two replicas coordinate transcode session ownership through Valkey and
# resume each other's HLS segments off the shared RWX transcode PVC. Stable
@@ -164,7 +162,7 @@ spec:
readOnly: true
containers:
- name: fafflix
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/jellyfin-ha:v0.4.0
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/jellyfin-ha:v0.2.0
imagePullPolicy: IfNotPresent
ports:
- name: http
+31 -3
View File
@@ -1,6 +1,11 @@
---
# HTTPS front for the k8s Gitea on git.k8s.syd1.au.unkin.net (external-dns
# k8s.syd1 zone). git.unkin.net stays on the VM haproxy until cutover.
# HTTPS front for the k8s Gitea, served on two names:
# git.unkin.net — canonical/production (apex, bind-operator zone;
# DNS flip is the gated cutover step, see the doc)
# git.k8s.syd1.au.unkin.net — admin/backup route (external-dns k8s.syd1 zone),
# same dual-name pattern as identity.unkin.net.
# The cert-manager Certificate (vault-issuer) takes CN git.unkin.net and gets a
# DNS SAN for each TLS listener hostname automatically.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
@@ -12,13 +17,36 @@ metadata:
traefik.io/instance: internal
annotations:
cert-manager.io/cluster-issuer: vault-issuer
cert-manager.io/common-name: git.k8s.syd1.au.unkin.net
cert-manager.io/common-name: git.unkin.net
cert-manager.io/private-key-size: "4096"
# Only the k8s admin route is published by external-dns (it owns just the
# k8s.syd1.au.unkin.net zone). git.unkin.net lives in the apex zone and is
# flipped at cutover — NOT managed here.
external-dns.alpha.kubernetes.io/hostname: git.k8s.syd1.au.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
spec:
gatewayClassName: traefik-internal
listeners:
- name: http-primary
port: 80
protocol: HTTP
hostname: git.unkin.net
allowedRoutes:
namespaces:
from: Same
- name: https-primary
port: 443
protocol: HTTPS
hostname: git.unkin.net
allowedRoutes:
namespaces:
from: Same
tls:
mode: Terminate
certificateRefs:
- group: ""
kind: Secret
name: gitea-tls
- name: http-admin
port: 80
protocol: HTTP
+10
View File
@@ -9,8 +9,13 @@ metadata:
app.kubernetes.io/instance: gitea
spec:
hostnames:
- git.unkin.net
- git.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: gitea
sectionName: http-primary
- group: gateway.networking.k8s.io
kind: Gateway
name: gitea
@@ -36,8 +41,13 @@ metadata:
app.kubernetes.io/instance: gitea
spec:
hostnames:
- git.unkin.net
- git.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: gitea
sectionName: https-primary
- group: gateway.networking.k8s.io
kind: Gateway
name: gitea
-15
View File
@@ -1,15 +0,0 @@
---
# k8up dashboard (M0NsTeRRR/grafana-dashboards), datasource resolved to the
# in-cluster VictoriaMetrics. Backed by the k8up VMServiceScrape.
apiVersion: grafana.integreatly.org/v1beta1
kind: GrafanaDashboard
metadata:
name: k8up
namespace: grafana
spec:
instanceSelector:
matchLabels:
dashboards: "grafana"
resyncPeriod: 5m
allowCrossNamespaceImport: false
gzipJson: H4sIAAAAAAACA+2ce3PbNhLAvwqHc3eTzlixqIdfSTvjptc2M7Hjq9Pc3NUeDUhCEiIIYAHQsqLRffZbAHzAop0qaSRHKf4SCZLAcrH7wwJYcREixrhCinAmw5NgEVIiFRz8tgjjnFD1ksFJtBeEKVJI8lwk2Nym5pk+CEcCDRFDIdyRk1SXtFrBT7YwaLXCJVzADMVU361EjuF8TFLnjCScveCUC/2wGMXoSXsv6ETQZqff3wuib3TdDE1Nc6e1tME/glOKhZL6eilOiuQ45kik4fJaN51imQiS6Qf05clRngXVPcFYqUye7O/r4qeE64pwStRdaYdEJoj+ByNxqZBQZ5ypMVwFIcMRw+qlfulOOzo42DPayMZvOKeKZMU9RikspxSOKWETreXfrs3JDT7nMzgdIip1SxlimEqr/IRTijKJU+f6SJD0gttuGhfdop/v9ODgtmhvrn+XZcPdO9XqZhVR5u3C1xkWSIHaHfUJkAeeXTzU25ngU6zGOJdOh6dYdeb9foqS3rtJYnr8QUlXBI0qQSOtPp7Vdpjw1LZNERvlaGTbp4gwhW+Vbl6O+ewVYfg8n8ZYSEdR+soZYeQMZWWpbggMTWGmrTvUz09tC+EUiUnKZ8xIntF8RNhbqK8wmaj9tPO0G7qaOwdjlBkC3TiqM1JtQ3edhu46R7uju8tkjNOcYhm847F8FAX2GgrstndHgW+AfxSqJRSn29dht9DhQUOHx7ujwu9RMoFxQMJAkj+O+o6b/NshE3wxxsnkMdUXNRmo44Vd0d+pSMYw+D+qBvtNDXZ2R4O/YAmRy+Nq8Kipwe7uaPBC5OxR9deJmvrr7V4cs00V9h6Ioju1Bg+3qsHnZqoEKphT/O1VOCOpGp9E7fbfnwVjTEZjZU9iLlIsTtrZbSA5JWkQUxiBn12F312JKxYEz1Va1aHFaiFKRuwkSKAZLJ7dwCSPwBSsLJ6SNKX4A7U+z6rqhiBsa2ZlCWJOU339b6wM4J/vZ6UM+yo1h3CgX+o71zrGakr/yDIEzjAyWsG3GRcKpwPmzhPs9R+IwEk5G73ZiNUMCabpC86GZGSeT/EQ5VSV9mAn2ovq3dRYYOhpmkrz9BRlGWGjerpYX3afQjHoPFfmzaTCWT1rtfP4mObm2g3SB3YCbGfkOSNGS4wzbFrk0L9g6Ng2uY7ddxp232lGnyDIWSGsFaKcnpelRgAofJdLRYbzshjlyiwEcEHA/FDZV2WxAK9P8Gu3KUQTIzv4mFTnXJ3Du4bXZU/IEjVGCul6ku7yhozgRPgVmvNcFUsQa9ndPXaFxAirol/+pEnpRZFanXpdCDzPrJbc4inQRDjUAOs3FiDz6RO9rjKQ5YxvMEL5CC+a7vHt/67C2ievwqVZ8iEM0Go4U1ZN8Qiz9EcuptbRBoOqV4BuzpKNwEOzLBOehstrx8c0q79AH2P8beEmYTv8dJ8bCYxZw+n2HIm09bo3RJ/NJXsNl+xG3iW35pJmmP+wPxpf1OsFA2VWEFrBfd7513K8Bxwtw0KHHmj0oKulEC62Vpyp6W0Nh4Ro6K7HFS2Z049zvNWFGGcsXPG7KVgpef+ziYGg4FDP/GzZv3W8VhZtwbl0QPmmVPkrFNtVaTfcrK6egYJtNFpYliTvoecqydYOxtaLwbbgmNWoGMTz4ElzEPwmeFK7qMyTBEs5zOkg4bmOgtcYNvf0cuAbEBfi3tgscemhNNgP1mzUcOGT29kQOSyVPDo2gI7jBjoOPTo8OhK9urt5clTNfOXgME+fEXaGbp3X+wpxEjWn5UeeJ54nyO52bJ4oTkM+GNk5evQb9Oh5enh6CLvTt3l6OA15euwcPY4a9Djw9PD0yPQu9+bZUTXjybFr5KjSEGpy9D05PDnKfZHNw8NtafP8+FN55Qd1okn/4cTyi1yOg5+QwjM030Ry+UcyLEMUK4VbCXibJLaKJIdgb2puQ7dEfm9SSS7Hd7Lx9YUXJgMFp//Fgq9ecrczy2QafcH4ZenDuuCCQp9Pi5SZsudiJE51OktRrjWcCjS71Hkr+j5K7FbokFD6GoyGqHn5pwOBUk2a1U1T/a+KH0XxVtZkHJFV9aeEsuSGvHfZAmaHhUGRyxQtxkutg4zTCm26EJn9f31UQlBbTcYJU5fkPS4wqv85gX8gUgkS58XjdYcX9ZRku9APS1dJ4C1sVSJwjmRiSbYAXfA8swbvpOtU+9j1+FTqtTYLPhyGy8+Y8XL/7vv1cr1h6ahwuWg1t+uw3tdbzRCu+tgZUvQLpERmFFW75+YPPYb2jhnGXGkHKPO/yprKze3aWmoFSFASta8PdKv1vNwq/w1r7a7TQE/ZSDJAN4hQnbM1kAzQNuZKLixE70ktWY+wi6qC5XIdzlaZvaUoQSVKADFKUNZ2J+GLgFQYggbpqeip+Fen4gKiGjmHKqa/4KHJuYQrl8Y9TPcZkRSEStaMrRvEc/NXrHA1YC+Ew7cJzS0+DAJMzD3B84RyNAlawX553Mq4VCCqbKX5NDOheAY+Tm6NEikNoCacqRMbAqP0NaPzkpYmkBY800mq5atY6aw7Pa0s0NHCR5iibeR6ud6wUW3COONG5MeNe8eNZKypng5SM53R0zc5SHMB4hY3fs5RBMxtschuko8bT15YEQNHxMCKGFgR/ejiRxc/unzmmDvq1/Dsenh+EJ7gKx+HzWGFR42oQcGoLfPUSO1J6knqSboRktZ/0HVQ2vMo/bwo9cT0xPTE/Lpiz063BmbfA/NeYDI8+5In7ed45ifsHpoemtsLM11qHnhqemp6anpqemp+ONTsOl8QPPTQvBeaOQMRCWDhi94m+rWS0iPUI9QjdHuBp8vQjmfoHzH0i1zidOj5qauc17Z+bZolW3Ra7xTVydXdY9MPjiWbT7Wo0lnq724nuRDWDBbLhz+8vW53AX8JIxUcNP4GNrN8NW95757k5rD+XLf5mDUzGTWnlNZ6pSVR73wWeQrMJ/VN5fe772ST185iFPJ7jsXcvOPvYv7GvmbklH+S8FWvX1T6eosE0Tl6/9L1/tNYc+tOmdFb3Z2RORvh26prJyT7VdDLOUtc9FmvazsmYiW3qTvaXMzLDe0oAN45ax2af0AoXpyH5Y0ZSSY206ksea99uWi/slytA6f34yg6Oj6O41a/E0et3rCNWqjX7bXifq/TTnv9XoRiw8DKKPVceYbxxHzZ3FS//D9scYEQEl4AAA==
-33
View File
@@ -37,36 +37,3 @@ spec:
- group: ""
kind: Secret
name: grafana-tls
---
# Public grafana.unkin.net via the external Traefik; TLS uses the reflected
# Let's Encrypt *.unkin.net wildcard, DNS lives in the bind-operator zone.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
labels:
traefik.io/instance: external
name: grafana-external
namespace: grafana
spec:
gatewayClassName: traefik-external
listeners:
- allowedRoutes:
namespaces:
from: Same
hostname: grafana.unkin.net
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
hostname: grafana.unkin.net
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: wildcard-unkin-net-tls
mode: Terminate
+3 -16
View File
@@ -9,16 +9,8 @@ metadata:
spec:
deployment:
spec:
replicas: 3
template:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: grafana
containers:
- name: grafana
env:
@@ -39,11 +31,11 @@ spec:
cpu: 100m
memory: 256Mi
limits:
cpu: "2"
memory: 4Gi
cpu: "1"
memory: 1Gi
config:
server:
root_url: "https://grafana.unkin.net"
root_url: "https://grafana.k8s.syd1.au.unkin.net"
database:
type: "postgres"
host: "postgres-pooler-rw.grafana.svc.cluster.local:5432"
@@ -69,8 +61,3 @@ spec:
# to akR-global-admin members (and direct members) via terraform-authentik.
role_attribute_path: "contains(ak_groups[*], 'akP-grafana-admin') && 'Admin' || 'Viewer'"
role_attribute_strict: "false"
# Gossip between replicas so each alert notification is sent once.
unified_alerting:
ha_peers: "grafana-alerting.grafana.svc.cluster.local:9094"
ha_listen_address: "${POD_IP}:9094"
ha_advertise_address: "${POD_IP}:9094"
-19
View File
@@ -20,22 +20,3 @@ spec:
jsonData:
timeInterval: "15s"
httpMethod: "POST"
---
apiVersion: grafana.integreatly.org/v1beta1
kind: GrafanaDatasource
metadata:
name: victorialogs
namespace: grafana
spec:
instanceSelector:
matchLabels:
dashboards: "grafana"
plugins:
- name: victoriametrics-logs-datasource
version: 0.32.0
datasource:
name: "VictoriaLogs"
type: "victoriametrics-logs-datasource"
uid: "victorialogs"
access: "proxy"
url: "http://vlselect-logs.logging.svc.cluster.local:9471"
-55
View File
@@ -53,58 +53,3 @@ spec:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: grafana-external-http-redirect
namespace: grafana
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
spec:
hostnames:
- grafana.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: grafana-external
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
matches:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: grafana-external
namespace: grafana
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
spec:
hostnames:
- grafana.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: grafana-external
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: grafana-service
port: 3000
weight: 1
matches:
- path:
type: PathPrefix
value: /
-1
View File
@@ -18,7 +18,6 @@ resources:
- dashboards/frr-ospf-route-metrics.yaml
- dashboards/gitea.yaml
- dashboards/haproxy.yaml
- dashboards/k8up.yaml
- dashboards/media-dashboard.yaml
- dashboards/node-exporter-full.yaml
- dashboards/nzbget.yaml
-256
View File
@@ -1,256 +0,0 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: haproxy-config
namespace: haproxy
data:
certificate.list: |
# First entry is the default cert for non-matching SNI.
/etc/haproxy/certs/unkin-net/tls.crt
/etc/haproxy/certs/main-unkin-net/tls.crt
/etc/haproxy/certs/ceph-unkin-net/tls.crt
fe_https.map: |
sonarr.main.unkin.net be_sonarr
radarr.main.unkin.net be_radarr
lidarr.main.unkin.net be_lidarr
readarr.main.unkin.net be_readarr
prowlarr.main.unkin.net be_prowlarr
nzbget.main.unkin.net be_nzbget
jellyfin.main.unkin.net be_jellyfin
fafflix.unkin.net be_jellyfin
git.unkin.net be_gitea
dashboard.ceph.unkin.net be_ceph_dashboard
auth.unkin.net be_k8s_kanidm
haproxy.cfg: |
global
log stdout format raw local0
log stdout format raw local1 notice
maxconn 4000
hard-stop-after 2m
ssl-default-bind-ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH
ssl-default-bind-options ssl-min-ver TLSv1.2 ssl-max-ver TLSv1.3
ssl-default-server-ciphers kEECDH+aRSA+AES:kRSA+AES:+AES256:RC4-SHA:!kEDH:!LOW:!EXP:!MD5:!aNULL:!eNULL
ssl-default-server-options no-sslv3
stats timeout 30s
stats socket /var/lib/haproxy/stats
stats socket /var/lib/haproxy/admin.sock mode 660 level admin
tune.ssl.default-dh-param 2048
defaults
log global
maxconn 5000
mode http
option httplog
option dontlognull
option http-server-close
option forwardfor except 127.0.0.0/8
option redispatch
retries 3
stats enable
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 5m
timeout server 5m
timeout http-keep-alive 10s
timeout check 10s
frontend fe_https
bind 0.0.0.0:443 ssl crt-list /usr/local/etc/haproxy/certificate.list ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH force-tlsv12
mode http
description Global HTTPS Frontend
http-request set-header X-Forwarded-Proto https
http-request set-header X-Real-IP %[src]
http-response set-header X-Content-Type-Options nosniff
http-response set-header X-XSS-Protection 1;mode=block
use_backend %[req.hdr(host),lower,map(/usr/local/etc/haproxy/fe_https.map,be_default)]
frontend fe_metrics
bind 0.0.0.0:8405
mode http
description Metrics Frontend
http-request set-header X-Forwarded-Proto https
http-request set-header X-Real-IP %[src]
http-request use-service prometheus-exporter if { path /metrics }
backend be_ceph_dashboard
description Backend for Ceph Dashboard from Mgr instances
balance roundrobin
cookie SRVNAME insert indirect nocache
http-check expect status 200
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { dst_port 9443 }
http-reuse always
option httpchk GET /
option forwardfor
option http-keep-alive
option prefer-last-server
redirect scheme https if !{ ssl_fc }
stick-table type ip size 200k expire 30m
server prodnxsr0009 198.18.23.9:9443 check cookie prodnxsr0009 fall 2 inter 2s rise 3 ssl verify none
server prodnxsr0010 198.18.23.10:9443 check cookie prodnxsr0010 fall 2 inter 2s rise 3 ssl verify none
server prodnxsr0011 198.18.23.11:9443 check cookie prodnxsr0011 fall 2 inter 2s rise 3 ssl verify none
server prodnxsr0012 198.18.23.12:9443 check cookie prodnxsr0012 fall 2 inter 2s rise 3 ssl verify none
server prodnxsr0013 198.18.23.13:9443 check cookie prodnxsr0013 fall 2 inter 2s rise 3 ssl verify none
backend be_default
description Backend for unmatched HTTP traffic
balance roundrobin
cookie SRVNAME insert
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { dst_port 443 }
option httpchk GET /
option forwardfor
backend be_gitea
description Backend for gitea cluster
balance roundrobin
cookie SRVNAME insert indirect nocache
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { dst_port 443 }
http-reuse always
option httpchk GET /
option forwardfor
option http-keep-alive
option prefer-last-server
redirect scheme https if !{ ssl_fc }
stick on src
stick-table type ip size 200k expire 30m
server ausyd1nxvm2080 198.18.26.18:443 check cookie ausyd1nxvm2080 fall 2 inter 2s rise 3 ssl verify none
server ausyd1nxvm2081 198.18.27.117:443 check cookie ausyd1nxvm2081 fall 2 inter 2s rise 3 ssl verify none
server ausyd1nxvm2082 198.18.28.71:443 check cookie ausyd1nxvm2082 fall 2 inter 2s rise 3 ssl verify none
backend be_jellyfin
description Backend for au-syd1 jellyfin
balance roundrobin
cookie SRVNAME insert indirect nocache
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { dst_port 443 }
http-reuse always
option httpchk GET /
option forwardfor
option http-keep-alive
option prefer-last-server
redirect scheme https if !{ ssl_fc }
server ausyd1nxvm2051 198.18.25.164:443 check cookie ausyd1nxvm2051 fall 2 inter 2s rise 3 ssl verify none
backend be_k8s_kanidm
description Backend for Kanidm (auth.unkin.net via Kubernetes internal Traefik)
balance roundrobin
http-reuse always
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { dst_port 443 }
redirect scheme https if !{ ssl_fc }
option httpchk
option forwardfor
option http-keep-alive
option prefer-last-server
http-check connect ssl sni auth.unkin.net
http-check send meth GET uri /status ver HTTP/1.1 hdr Host auth.unkin.net
http-check expect status 200
server k8s-traefik-internal 198.18.200.4:443 ssl verify none check inter 2s rise 3 fall 2 sni str(auth.unkin.net)
backend be_lidarr
description Backend for au-syd1 lidarr
balance roundrobin
cookie SRVNAME insert indirect nocache
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { dst_port 443 }
http-reuse always
option httpchk GET /consul/health
option forwardfor
option http-keep-alive
option prefer-last-server
redirect scheme https if !{ ssl_fc }
server ausyd1nxvm2048 198.18.28.165:443 check cookie ausyd1nxvm2048 fall 2 inter 2s rise 3 ssl verify none
backend be_nzbget
description Backend for au-syd1 nzbget
balance roundrobin
cookie SRVNAME insert indirect nocache
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { dst_port 443 }
http-reuse always
option httpchk GET /consul/health
option forwardfor
option http-keep-alive
option prefer-last-server
redirect scheme https if !{ ssl_fc }
server ausyd1nxvm2045 198.18.25.44:443 check cookie ausyd1nxvm2045 fall 2 inter 2s rise 3 ssl verify none
backend be_prowlarr
description Backend for au-syd1 prowlarr
balance roundrobin
cookie SRVNAME insert indirect nocache
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { dst_port 443 }
http-reuse always
option httpchk GET /consul/health
option forwardfor
option http-keep-alive
option prefer-last-server
redirect scheme https if !{ ssl_fc }
server ausyd1nxvm2050 198.18.25.66:443 check cookie ausyd1nxvm2050 fall 2 inter 2s rise 3 ssl verify none
backend be_radarr
description Backend for au-syd1 radarr
balance roundrobin
cookie SRVNAME insert indirect nocache
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { dst_port 443 }
http-reuse always
option httpchk GET /consul/health
option forwardfor
option http-keep-alive
option prefer-last-server
redirect scheme https if !{ ssl_fc }
server ausyd1nxvm2047 198.18.27.131:443 check cookie ausyd1nxvm2047 fall 2 inter 2s rise 3 ssl verify none
backend be_readarr
description Backend for au-syd1 readarr
balance roundrobin
cookie SRVNAME insert indirect nocache
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { dst_port 443 }
http-reuse always
option httpchk GET /consul/health
option forwardfor
option http-keep-alive
option prefer-last-server
redirect scheme https if !{ ssl_fc }
server ausyd1nxvm2049 198.18.29.32:443 check cookie ausyd1nxvm2049 fall 2 inter 2s rise 3 ssl verify none
backend be_sonarr
description Backend for au-syd1 sonarr
balance roundrobin
cookie SRVNAME insert indirect nocache
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { dst_port 443 }
http-reuse always
option httpchk GET /consul/health
option forwardfor
option http-keep-alive
option prefer-last-server
redirect scheme https if !{ ssl_fc }
server ausyd1nxvm2046 198.18.26.161:443 check cookie ausyd1nxvm2046 fall 2 inter 2s rise 3 ssl verify none
# The `peers au-syd1-prod` section is dropped: peer names must be static and a
# Deployment cannot provide them. Behind the external Traefik's TLS
# passthrough `src` is a Traefik pod, so X-Real-IP, forwardfor and the
# `stick on src` tables all key on that; the SRVNAME cookie carries real
# session persistence. Traefik cannot emit PROXY protocol to a TLSRoute
# backend, so there is nothing to bind `accept-proxy` to.
listen health
bind 0.0.0.0:8404
mode http
monitor-uri /healthz
listen stats
bind 127.0.0.1:9090
mode http
stats uri /
stats auth admin:admin
-148
View File
@@ -1,148 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: haproxy
namespace: haproxy
annotations:
reloader.stakater.com/auto: "true"
spec:
replicas: 3
selector:
matchLabels:
app: haproxy
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
template:
metadata:
labels:
app: haproxy
spec:
automountServiceAccountToken: false
terminationGracePeriodSeconds: 150
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: haproxy
topologyKey: kubernetes.io/hostname
securityContext:
runAsNonRoot: true
runAsUser: 99
runAsGroup: 99
seccompProfile:
type: RuntimeDefault
containers:
- name: haproxy
image: haproxy:3.2.24-alpine
imagePullPolicy: IfNotPresent
command:
- haproxy
- -W
- -db
- -f
- /usr/local/etc/haproxy/haproxy.cfg
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
# fe_https binds the privileged port 443 as uid 99, and the
# dst_port ACLs need the real port.
add: [NET_BIND_SERVICE]
ports:
- name: https
containerPort: 443
protocol: TCP
- name: health
containerPort: 8404
protocol: TCP
- name: metrics
containerPort: 8405
protocol: TCP
- name: stats
containerPort: 9090
protocol: TCP
lifecycle:
preStop:
exec:
# SIGUSR1 to the master soft-stops the workers; hard-stop-after
# caps the drain. Wait so kubelet holds SIGTERM until it is done.
command:
- /bin/sh
- -c
- kill -s USR1 1; while kill -0 1 2>/dev/null; do sleep 1; done
livenessProbe:
httpGet:
path: /healthz
port: health
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: health
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 2
memory: 1Gi
volumeMounts:
- name: config
mountPath: /usr/local/etc/haproxy
readOnly: true
- name: cert-unkin-net
mountPath: /etc/haproxy/certs/unkin-net
readOnly: true
- name: cert-main-unkin-net
mountPath: /etc/haproxy/certs/main-unkin-net
readOnly: true
- name: cert-ceph-unkin-net
mountPath: /etc/haproxy/certs/ceph-unkin-net
readOnly: true
- name: run
mountPath: /var/lib/haproxy
volumes:
- name: config
configMap:
name: haproxy-config
# ssl-load-extra-files loads <crtfile>.key by default, so the key is
# projected next to the cert as tls.crt.key.
- name: cert-unkin-net
secret:
secretName: wildcard-unkin-net-tls
items:
- key: tls.crt
path: tls.crt
- key: tls.key
path: tls.crt.key
- name: cert-main-unkin-net
secret:
secretName: wildcard-main-unkin-net-tls
items:
- key: tls.crt
path: tls.crt
- key: tls.key
path: tls.crt.key
- name: cert-ceph-unkin-net
secret:
secretName: wildcard-ceph-unkin-net-tls
items:
- key: tls.crt
path: tls.crt
- key: tls.key
path: tls.crt.key
- name: run
emptyDir: {}
restartPolicy: Always
-31
View File
@@ -1,31 +0,0 @@
---
# External (DMZ) front for the haproxy edge on the traefik-external LB VIP
# 198.18.199.0. The :443 listener is TLS Passthrough: haproxy owns the three
# wildcard certs and terminates behind Traefik, so there are no certificateRefs
# here. Listener hostnames are deliberately unset and the routes carry the
# explicit hostname list instead; allowedRoutes Same keeps other namespaces off
# these listeners.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: haproxy
namespace: haproxy
labels:
traefik.io/instance: external
spec:
gatewayClassName: traefik-external
listeners:
- name: http
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: Same
- name: https-passthrough
port: 443
protocol: TLS
tls:
mode: Passthrough
allowedRoutes:
namespaces:
from: Same
-36
View File
@@ -1,36 +0,0 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: haproxy-http-redirect
namespace: haproxy
labels:
app: haproxy
spec:
hostnames:
- sonarr.main.unkin.net
- radarr.main.unkin.net
- lidarr.main.unkin.net
- readarr.main.unkin.net
- prowlarr.main.unkin.net
- nzbget.main.unkin.net
- jellyfin.main.unkin.net
- fafflix.unkin.net
- git.unkin.net
- dashboard.ceph.unkin.net
- auth.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: haproxy
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
matches:
- path:
type: PathPrefix
value: /
-15
View File
@@ -1,15 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- configmap.yaml
- deployment.yaml
- service.yaml
- gateway.yaml
- tlsroute.yaml
- httproute.yaml
- pdb.yaml
- vpa.yaml
- vmpodscrape.yaml
-5
View File
@@ -1,5 +0,0 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: haproxy
-11
View File
@@ -1,11 +0,0 @@
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: haproxy
namespace: haproxy
spec:
maxUnavailable: 1
selector:
matchLabels:
app: haproxy
-19
View File
@@ -1,19 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: haproxy
namespace: haproxy
spec:
type: ClusterIP
# Reached only by the external Traefik's TLS-passthrough TLSRoute, so the
# peer address here is a Traefik pod, not the client. sessionAffinity is
# deliberately absent: keyed on ClientIP it would pin whole Traefik pods,
# not clients. Backend persistence rests on the per-backend SRVNAME cookie.
selector:
app: haproxy
ports:
- name: https
port: 443
protocol: TCP
targetPort: https
-33
View File
@@ -1,33 +0,0 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: TLSRoute
metadata:
name: haproxy
namespace: haproxy
labels:
app: haproxy
spec:
hostnames:
- sonarr.main.unkin.net
- radarr.main.unkin.net
- lidarr.main.unkin.net
- readarr.main.unkin.net
- prowlarr.main.unkin.net
- nzbget.main.unkin.net
- jellyfin.main.unkin.net
- fafflix.unkin.net
- git.unkin.net
- dashboard.ceph.unkin.net
- auth.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: haproxy
sectionName: https-passthrough
rules:
- backendRefs:
- group: ""
kind: Service
name: haproxy
port: 443
weight: 1
-13
View File
@@ -1,13 +0,0 @@
---
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
name: haproxy
namespace: haproxy
spec:
selector:
matchLabels:
app: haproxy
podMetricsEndpoints:
- port: metrics
path: /metrics
-13
View File
@@ -1,13 +0,0 @@
---
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: haproxy-vpa
namespace: haproxy
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: haproxy
updatePolicy:
updateMode: "Off"
-1
View File
@@ -4,4 +4,3 @@ kind: Kustomization
resources:
- namespace.yaml
- vmservicescrape.yaml
@@ -1,15 +0,0 @@
---
# Scrape the k8up operator metrics (k8up-metrics Service, port http :8080).
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMServiceScrape
metadata:
name: k8up
namespace: k8up-system
spec:
selector:
matchLabels:
app.kubernetes.io/name: k8up
app.kubernetes.io/instance: k8up
endpoints:
- port: http
path: /metrics
+43
View File
@@ -0,0 +1,43 @@
---
# S3 bucket (Ceph RGW) for the long-term raw-log archive, provisioned by the
# in-estate cephrgw-operator. The archiver Vector deployment writes here.
apiVersion: ceph.unkin.net/v1alpha1
kind: ObjectStoreUser
metadata:
name: logs-archive-owner
namespace: logging
spec:
displayName: "Logging raw-archive bucket owner"
maxBuckets: 5
quota:
enabled: true
# 5 TiB soft cap; real retention is enforced RGW-side by a bucket lifecycle
# policy (see PR notes) — the operator does not manage lifecycle.
maxSizeBytes: 5497558138880
---
apiVersion: ceph.unkin.net/v1alpha1
kind: Bucket
metadata:
name: logs-archive
namespace: logging
spec:
bucketName: logs-archive
ownerRef: logs-archive-owner
versioning: false
tags:
app: logging
purpose: raw-log-archive
# Keep the bucket (and its objects) if this CR is ever deleted.
retainOnDelete: true
---
apiVersion: ceph.unkin.net/v1alpha1
kind: BucketAccess
metadata:
name: logs-archive-writer
namespace: logging
spec:
bucketRef: logs-archive
level: read-write
# Operator writes AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (+ RGW_UID,
# S3_ENDPOINT, BUCKET_NAME) into this Secret; the archiver consumes it.
secretName: logs-archive-s3
@@ -0,0 +1,97 @@
---
apiVersion: clickhouse.altinity.com/v1
kind: ClickHouseInstallation
metadata:
name: logs
namespace: logging
spec:
defaults:
templates:
dataVolumeClaimTemplate: data-volume
serviceTemplate: chi-service
podTemplate: clickhouse
configuration:
users:
# Password hash is sourced from the Vault-synced clickhouse-credentials
# Secret; the plaintext never lands in git or the ClickHouse config.
vector/password_sha256_hex:
valueFrom:
secretKeyRef:
name: clickhouse-credentials
key: password_sha256_hex
vector/networks/ip:
- "::/0"
vector/profile: default
vector/quota: default
# Allow the vector user to create the logs database/table (bootstrap Job)
# and to INSERT. Restrict the built-in default user to loopback only.
vector/access_management: "1"
default/networks/ip:
- "127.0.0.1"
- "::1"
# Read-only user for CLI tools + the logviewer UI. Hash sourced from the
# Vault-synced clickhouse-logreader Secret, same mechanism as vector.
# Scoped to the logs database only (unlike vector, which bootstraps it).
logreader/password_sha256_hex:
valueFrom:
secretKeyRef:
name: clickhouse-logreader
key: password_sha256_hex
logreader/networks/ip:
- "::/0"
logreader/profile: readonly
logreader/quota: default
logreader/allow_databases/database:
- "logs"
profiles:
default/max_memory_usage: "10000000000"
default/max_execution_time: "120"
readonly/readonly: "2"
readonly/max_memory_usage: "10000000000"
readonly/max_execution_time: "120"
clusters:
- name: logs
layout:
shardsCount: 1
replicasCount: 1
templates:
volumeClaimTemplates:
- name: data-volume
spec:
storageClassName: cephrbd-fast-delete
accessModes:
- ReadWriteOnce
resources:
requests:
# 3d TTL on logs.raw. At ~130 GiB/day raw, ClickHouse LZ4/ZSTD
# (~6x on log text) stores ~20-25 GiB/day => ~60-75 GiB/3d, plus
# merge headroom (~2x peak). logs.raw is the only table. 150Gi
# gives comfortable headroom; long-term data lives in S3, not here.
storage: 150Gi
serviceTemplates:
- name: chi-service
generateName: "clickhouse-{chi}"
spec:
type: ClusterIP
ports:
- name: http
port: 8123
- name: tcp
port: 9000
podTemplates:
- name: clickhouse
spec:
securityContext:
fsGroup: 101
runAsUser: 101
runAsGroup: 101
containers:
- name: clickhouse
image: docker.io/clickhouse/clickhouse-server:24.8
resources:
requests:
cpu: 500m
memory: 2Gi
limits:
cpu: "2"
memory: 8Gi
@@ -0,0 +1,40 @@
---
# logarchiver non-secret config. Secrets (NATS/S3/ClickHouse creds) and the
# subject filter come from env; everything else uses the binary's built-in
# defaults, which already target this stack. ack_wait MUST exceed batch.max_age
# so unacked messages in an open batch are not redelivered mid-batch.
apiVersion: v1
kind: ConfigMap
metadata:
name: logarchiver-config
namespace: logging
data:
config.yaml: |
nats:
ack_wait: 5m
fetch_batch: 512
batch:
max_bytes: 67108864
max_events: 200000
max_age: 2m
# Pin the proven RGW endpoint/bucket; ignore the secret's S3_ENDPOINT/BUCKET_NAME
# (AWS creds still come from the secret env). endpoint_env/bucket_env off.
s3:
endpoint: "https://s3.ceph.unkin.net"
bucket: "logs-archive"
region: "us-east-1"
path_style: true
ca_file: /etc/vault-ca/ca.crt
endpoint_env: ""
bucket_env: ""
crypto:
key_name: logarchive
pubkey_source: vault
vault:
address: "https://vault.service.consul:8200"
mount: gpg
auth_method: kubernetes
k8s_mount: k8s/au/syd1
k8s_role: logging_logarchiver
k8s_jwt_path: /var/run/secrets/vault/token
ca_file: /etc/vault-ca/ca.crt
@@ -0,0 +1,134 @@
---
# logarchiver — replaces the vector-archiver leg. Independent JetStream durable
# consumer (archiver) that seals raw logs to S3 as zstd + OpenPGP objects and
# indexes each object in ClickHouse (logs.archive_index). Acks only after the
# object is in S3 AND indexed. Reuses the same NATS/S3/CA wiring the Vector
# archiver used; the OpenPGP public key is delivered as a mounted file.
apiVersion: apps/v1
kind: Deployment
metadata:
name: logarchiver
namespace: logging
annotations:
configmap.reloader.stakater.com/auto: "true"
secret.reloader.stakater.com/reload: "vault-ca-cert"
labels:
app.kubernetes.io/name: logarchiver
app.kubernetes.io/component: archiver
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: logarchiver
template:
metadata:
labels:
app.kubernetes.io/name: logarchiver
vector.dev/exclude: "true"
spec:
# Dedicated SA whose projected vault-audience token authenticates the
# k8s-auth login used to fetch the logarchive public key from the gpg engine.
serviceAccountName: logarchiver
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: logarchiver
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/logarchiver:v0.1.0
imagePullPolicy: IfNotPresent
args: ["run"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
ports:
- containerPort: 9090
name: metrics
protocol: TCP
env:
- name: LOGARCHIVER_CONFIG
value: /etc/logarchiver/config.yaml
# Server-side subject filter; must match the archiver consumer's filter.
- name: ARCHIVE_SUBJECTS
value: "logs.k8s.vault.>"
- name: NATS_CONSUMER_PASSWORD
valueFrom:
secretKeyRef:
name: nats-auth
key: consumer_password
- name: CLICKHOUSE_USER
valueFrom:
secretKeyRef:
name: clickhouse-credentials
key: username
- name: CLICKHOUSE_PASSWORD
valueFrom:
secretKeyRef:
name: clickhouse-credentials
key: password
# S3 creds + S3_ENDPOINT + BUCKET_NAME from the cephrgw BucketAccess Secret.
envFrom:
- secretRef:
name: logs-archive-s3
livenessProbe:
httpGet:
path: /healthz
port: metrics
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: metrics
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
volumeMounts:
- name: config
mountPath: /etc/logarchiver/config.yaml
subPath: config.yaml
readOnly: true
- name: vault-token
mountPath: /var/run/secrets/vault
readOnly: true
- name: vault-ca-cert
mountPath: /etc/vault-ca/ca.crt
subPath: ca.crt
readOnly: true
- name: tmp
mountPath: /tmp
volumes:
- name: config
configMap:
name: logarchiver-config
# Projected SA token with audience "vault" for the gpg-engine k8s login.
- name: vault-token
projected:
sources:
- serviceAccountToken:
path: token
audience: vault
expirationSeconds: 600
- name: vault-ca-cert
secret:
secretName: vault-ca-cert
- name: tmp
emptyDir: {}
+6 -43
View File
@@ -1,13 +1,16 @@
---
# Log ingestion endpoint for puppet-managed VMs (and any non-k8s client):
# fronts the VLCluster vlinsert service over TLS at a name VMs can resolve.
# Log ingestion endpoint for puppet-managed VMs (and any non-k8s client).
# Reuses the internal Traefik gateway + cert-manager + external-dns pattern so
# VMs reach the Vector aggregator's HTTP source over TLS at a DNS name they can
# resolve. The puppet-side Vector rollout ships NDJSON to
# https://logs-ingest.k8s.syd1.au.unkin.net/ (a later task).
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: logs-ingest
namespace: logging
labels:
app.kubernetes.io/name: victorialogs
app.kubernetes.io/name: vector-aggregator
app.kubernetes.io/component: ingest
traefik.io/instance: internal
annotations:
@@ -39,43 +42,3 @@ spec:
- group: ""
kind: Secret
name: logs-ingest-tls
---
# Unauthenticated LogsQL query endpoint fronting the VLCluster vlselect service.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: vlselect
namespace: logging
labels:
app.kubernetes.io/name: victorialogs
app.kubernetes.io/component: query
traefik.io/instance: internal
annotations:
cert-manager.io/cluster-issuer: vault-issuer
cert-manager.io/common-name: vlselect.k8s.syd1.au.unkin.net
cert-manager.io/private-key-size: "4096"
external-dns.alpha.kubernetes.io/hostname: vlselect.k8s.syd1.au.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
spec:
gatewayClassName: traefik-internal
listeners:
- name: http
port: 80
protocol: HTTP
hostname: vlselect.k8s.syd1.au.unkin.net
allowedRoutes:
namespaces:
from: Same
- name: https
port: 443
protocol: HTTPS
hostname: vlselect.k8s.syd1.au.unkin.net
allowedRoutes:
namespaces:
from: Same
tls:
mode: Terminate
certificateRefs:
- group: ""
kind: Secret
name: vlselect-tls
+4 -59
View File
@@ -5,7 +5,7 @@ metadata:
name: logs-ingest-http-redirect
namespace: logging
labels:
app.kubernetes.io/name: victorialogs
app.kubernetes.io/name: vector-aggregator
app.kubernetes.io/component: ingest
spec:
hostnames:
@@ -32,7 +32,7 @@ metadata:
name: logs-ingest
namespace: logging
labels:
app.kubernetes.io/name: victorialogs
app.kubernetes.io/name: vector-aggregator
app.kubernetes.io/component: ingest
spec:
hostnames:
@@ -46,63 +46,8 @@ spec:
- backendRefs:
- group: ""
kind: Service
name: vlinsert-logs
port: 9481
weight: 1
matches:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: vlselect-http-redirect
namespace: logging
labels:
app.kubernetes.io/name: victorialogs
app.kubernetes.io/component: query
spec:
hostnames:
- vlselect.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: vlselect
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
matches:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: vlselect
namespace: logging
labels:
app.kubernetes.io/name: victorialogs
app.kubernetes.io/component: query
spec:
hostnames:
- vlselect.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: vlselect
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: vlselect-logs
port: 9471
name: vector-vm-ingest
port: 8080
weight: 1
matches:
- path:
@@ -0,0 +1,131 @@
---
# Declarative ClickHouse schema bootstrap. Runs as an ArgoCD PostSync hook so it
# executes after the ClickHouseInstallation is reconciled, and re-runs on every
# sync (idempotent CREATE ... IF NOT EXISTS). Edit the DDL here to evolve the
# schema; the Vector aggregator writes to logs.raw with skip_unknown_fields, so
# adding columns is backward-compatible.
apiVersion: batch/v1
kind: Job
metadata:
name: clickhouse-schema
namespace: logging
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
labels:
app.kubernetes.io/name: clickhouse-schema
app.kubernetes.io/component: bootstrap
spec:
backoffLimit: 20
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app.kubernetes.io/name: clickhouse-schema
vector.dev/exclude: "true"
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
containers:
- name: clickhouse-schema
image: docker.io/clickhouse/clickhouse-server:24.8
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
env:
- name: HOME
value: /tmp
- name: CLICKHOUSE_USER
valueFrom:
secretKeyRef:
name: clickhouse-credentials
key: username
- name: CLICKHOUSE_PASSWORD
valueFrom:
secretKeyRef:
name: clickhouse-credentials
key: password
command:
- /bin/bash
- -ec
- |
host=clickhouse-logs.logging.svc.cluster.local
echo "Waiting for ClickHouse at ${host}:9000 ..."
until clickhouse-client --host "$host" --port 9000 \
--user "$CLICKHOUSE_USER" --password "$CLICKHOUSE_PASSWORD" \
--query "SELECT 1" >/dev/null 2>&1; do
echo " not ready, retrying in 5s"; sleep 5
done
echo "Applying schema ..."
clickhouse-client --host "$host" --port 9000 \
--user "$CLICKHOUSE_USER" --password "$CLICKHOUSE_PASSWORD" \
--multiquery <<'EOSQL'
CREATE DATABASE IF NOT EXISTS logs;
CREATE TABLE IF NOT EXISTS logs.raw
(
timestamp DateTime64(3) DEFAULT now64(3),
host LowCardinality(String) DEFAULT '',
source LowCardinality(String) DEFAULT '',
namespace LowCardinality(String) DEFAULT '',
pod String DEFAULT '',
container LowCardinality(String) DEFAULT '',
stream LowCardinality(String) DEFAULT '',
severity LowCardinality(String) DEFAULT '',
message String DEFAULT '',
labels Map(LowCardinality(String), String),
fields Map(LowCardinality(String), String)
)
ENGINE = MergeTree
PARTITION BY toDate(timestamp)
ORDER BY (source, namespace, host, timestamp)
TTL toDateTime(timestamp) + INTERVAL 3 DAY
SETTINGS index_granularity = 8192;
-- One row per archived S3 object (written by logarchiver). No TTL:
-- the index must outlive logs.raw so the long-term S3 archive stays
-- searchable. Keep in sync with logarchiver internal/index/ddl.go.
CREATE TABLE IF NOT EXISTS logs.archive_index
(
object_key String,
bucket LowCardinality(String),
subject LowCardinality(String),
hosts Array(LowCardinality(String)),
min_ts DateTime64(3),
max_ts DateTime64(3),
event_count UInt64,
raw_bytes UInt64,
stored_bytes UInt64,
compression LowCardinality(String),
cipher LowCardinality(String),
container_format LowCardinality(String),
key_name LowCardinality(String),
key_fingerprint String,
created_at DateTime64(3) DEFAULT now64(3),
INDEX idx_hosts hosts TYPE bloom_filter GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(min_ts)
ORDER BY (subject, min_ts, object_key);
EOSQL
echo "Schema applied."
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
+41 -2
View File
@@ -5,7 +5,46 @@ kind: Kustomization
resources:
- namespace.yaml
- vaultauth.yaml
- vlcluster.yaml
- vlagent.yaml
- vaultstaticsecret.yaml
- clickhouseinstallation.yaml
- job_clickhouse-schema.yaml
- nats-bootstrap-job.yaml
- cephrgw.yaml
- gateway.yaml
- httproute.yaml
- serviceaccount_logarchiver.yaml
- configmap_logarchiver.yaml
- deployment_logarchiver.yaml
- logviewer
# Vector pipelines are the single source of truth (also validated by
# `vector test` in CI). Mounted into each tier via `existingConfigMaps`.
configMapGenerator:
# Tunable JetStream stream limits (the nats-bootstrap Job reads these and does
# create-or-update). Hash suffix is INTENTIONALLY left on: editing a value
# renames the ConfigMap, which rewrites the Job's env reference, which changes
# the PostSync hook Job's spec and forces Argo to re-run it -> new limits apply.
# Sizing assumes ~1500 events/s avg @ ~1 KiB/event with S2 compression (~4x):
# ~33 GiB/day compressed -> ~100 GiB/3d per replica. max_bytes 130 GiB sits
# under the 180Gi/node PVC (see values-nats.yaml). Raising retention beyond the
# PVC requires bumping BOTH max_bytes here and fileStore PVC size in values.
- name: nats-stream-limits
literals:
- max_age=72h
- max_bytes=139586437120
- dupe_window=2m
- name: vector-agent-config
files:
- agent.yaml=vector/agent.yaml
options:
disableNameSuffixHash: true
- name: vector-aggregator-config
files:
- aggregator.yaml=vector/aggregator.yaml
options:
disableNameSuffixHash: true
- name: vector-vm-ingest-config
files:
- vm-ingest.yaml=vector/vm-ingest.yaml
options:
disableNameSuffixHash: true
@@ -0,0 +1,80 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: logviewer
namespace: logging
annotations:
secret.reloader.stakater.com/reload: "clickhouse-logreader"
spec:
replicas: 1
selector:
matchLabels:
app: logviewer
strategy:
type: Recreate
template:
metadata:
labels:
app: logviewer
spec:
serviceAccountName: default
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: logviewer
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/logviewer:v0.1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: CH_URL
value: http://clickhouse-logs.logging.svc.cluster.local:8123
- name: CH_USER
valueFrom:
secretKeyRef:
name: clickhouse-logreader
key: username
- name: CH_PASSWORD
valueFrom:
secretKeyRef:
name: clickhouse-logreader
key: password
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
restartPolicy: Always
+38
View File
@@ -0,0 +1,38 @@
---
# Internal front for the logviewer UI (cf. mediamover/pdbmux).
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
labels:
traefik.io/instance: internal
annotations:
cert-manager.io/cluster-issuer: vault-issuer
cert-manager.io/common-name: logviewer.unkin.net
cert-manager.io/private-key-size: "4096"
external-dns.alpha.kubernetes.io/hostname: logviewer.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
name: logviewer
namespace: logging
spec:
gatewayClassName: traefik-internal
listeners:
- allowedRoutes:
namespaces:
from: Same
hostname: logviewer.unkin.net
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
hostname: logviewer.unkin.net
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: logviewer-tls
mode: Terminate
@@ -2,15 +2,15 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: vlogs-http-redirect
namespace: vlogs
name: logviewer-http-redirect
namespace: logging
spec:
hostnames:
- vlogs.unkin.net
- logviewer.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: vlogs-external
name: logviewer
sectionName: http
rules:
- filters:
@@ -26,33 +26,21 @@ spec:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: vlogs
namespace: vlogs
name: logviewer
namespace: logging
spec:
hostnames:
- vlogs.unkin.net
- logviewer.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: vlogs-external
name: logviewer
sectionName: https
rules:
# Exact / outranks the PathPrefix catch-all; target needs the trailing slash
- filters:
- type: RequestRedirect
requestRedirect:
path:
type: ReplaceFullPath
replaceFullPath: /select/vmui/
statusCode: 302
matches:
- path:
type: Exact
value: /
- backendRefs:
- group: ""
kind: Service
name: vlogs-oauth2
name: logviewer-oauth2
port: 80
weight: 1
matches:
@@ -3,9 +3,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- vaultauth.yaml
- vaultstaticsecret.yaml
- deployment.yaml
- oauth2-proxy-configmap.yaml
- oauth2-proxy-deployment.yaml
- service.yaml
@@ -1,27 +1,31 @@
---
# Non-secret oauth2-proxy configuration (client_id/secret/cookie_secret come
# from the logviewer-oauth-credentials Secret). Single auth front for the
# logviewer UI: everything requires an Authentik session in akP-logviewer-admin.
apiVersion: v1
kind: ConfigMap
metadata:
name: vlogs-oauth2-env
namespace: vlogs
name: logviewer-oauth2-env
namespace: logging
data:
OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180"
OAUTH2_PROXY_PROVIDER: "oidc"
OAUTH2_PROXY_OIDC_ISSUER_URL: "https://identity.unkin.net/application/o/vlogs/"
OAUTH2_PROXY_REDIRECT_URL: "https://vlogs.unkin.net/oauth2/callback"
OAUTH2_PROXY_UPSTREAMS: "http://vlselect-logs.logging.svc.cluster.local:9471/"
# Authentik logviewer app discovery issuer (terraform-authentik PR #21).
OAUTH2_PROXY_OIDC_ISSUER_URL: "https://identity.unkin.net/application/o/logviewer/"
OAUTH2_PROXY_REDIRECT_URL: "https://logviewer.unkin.net/oauth2/callback"
OAUTH2_PROXY_UPSTREAMS: "http://logviewer.logging.svc.cluster.local:8080/"
OAUTH2_PROXY_SCOPE: "openid email profile ak_groups"
# Populate session.Groups from the Authentik ak_groups claim.
OAUTH2_PROXY_OIDC_GROUPS_CLAIM: "ak_groups"
OAUTH2_PROXY_ALLOWED_GROUPS: "akP-vlogs-admin"
OAUTH2_PROXY_ALLOWED_GROUPS: "akP-logviewer-admin"
OAUTH2_PROXY_PASS_USER_HEADERS: "true"
OAUTH2_PROXY_EMAIL_DOMAINS: "*"
# Authentik hardcodes email_verified=false in the id_token; authorization is
# enforced via ak_groups, so accepting the unverified email is safe.
OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "true"
OAUTH2_PROXY_COOKIE_SECURE: "true"
OAUTH2_PROXY_COOKIE_DOMAINS: "vlogs.unkin.net"
OAUTH2_PROXY_WHITELIST_DOMAINS: "vlogs.unkin.net"
OAUTH2_PROXY_COOKIE_DOMAINS: "logviewer.unkin.net"
OAUTH2_PROXY_WHITELIST_DOMAINS: "logviewer.unkin.net"
OAUTH2_PROXY_REVERSE_PROXY: "true"
OAUTH2_PROXY_PROVIDER_CA_FILES: "/etc/ssl/combined/ca-certificates.crt"
OAUTH2_PROXY_CODE_CHALLENGE_METHOD: "S256"
@@ -2,16 +2,16 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: vlogs-oauth2
namespace: vlogs
name: logviewer-oauth2
namespace: logging
annotations:
configmap.reloader.stakater.com/auto: "true"
secret.reloader.stakater.com/reload: "vlogs-oauth-credentials,vault-ca-cert"
secret.reloader.stakater.com/reload: "logviewer-oauth-credentials,vault-ca-cert"
spec:
replicas: 1
selector:
matchLabels:
app: vlogs-oauth2
app: logviewer-oauth2
strategy:
rollingUpdate:
maxUnavailable: 1
@@ -19,7 +19,7 @@ spec:
template:
metadata:
labels:
app: vlogs-oauth2
app: logviewer-oauth2
spec:
serviceAccountName: default
automountServiceAccountToken: false
@@ -69,23 +69,23 @@ spec:
protocol: TCP
envFrom:
- configMapRef:
name: vlogs-oauth2-env
name: logviewer-oauth2-env
optional: false
env:
- name: OAUTH2_PROXY_CLIENT_ID
valueFrom:
secretKeyRef:
name: vlogs-oauth-credentials
name: logviewer-oauth-credentials
key: client_id
- name: OAUTH2_PROXY_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: vlogs-oauth-credentials
name: logviewer-oauth-credentials
key: client_secret
- name: OAUTH2_PROXY_COOKIE_SECRET
valueFrom:
secretKeyRef:
name: vlogs-oauth-credentials
name: logviewer-oauth-credentials
key: cookie_secret
volumeMounts:
- name: combined-certs
+36
View File
@@ -0,0 +1,36 @@
---
apiVersion: v1
kind: Service
metadata:
name: logviewer
namespace: logging
spec:
internalTrafficPolicy: Cluster
ports:
- name: http
port: 8080
protocol: TCP
targetPort: http
selector:
app: logviewer
sessionAffinity: None
type: ClusterIP
---
# Front-door entry Service: the HTTPRoute for logviewer.unkin.net targets this;
# all traffic enters via oauth2-proxy.
apiVersion: v1
kind: Service
metadata:
name: logviewer-oauth2
namespace: logging
spec:
internalTrafficPolicy: Cluster
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
selector:
app: logviewer-oauth2
sessionAffinity: None
type: ClusterIP
@@ -0,0 +1,21 @@
---
# Authentik OIDC client for logviewer (client_id, client_secret, cookie_secret)
# seeded at kv/kubernetes/namespace/logging/default/oauth-credentials; the
# logging/default templated policy already grants read, so no terraform-vault
# change is needed.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: logviewer-oauth-credentials
namespace: logging
spec:
destination:
create: true
name: logviewer-oauth-credentials
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/logging/default/oauth-credentials
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
+158
View File
@@ -0,0 +1,158 @@
---
# Declarative JetStream provisioning: the LOGS stream + durable consumers.
# ArgoCD PostSync hook, idempotent create-or-UPDATE, re-runs each sync.
#
# Stream LOGS: file storage, 3 replicas, retention=limits (NOT workqueue) so the
# transform tier AND the archiver each independently see every message — reading
# never deletes; only max-age/max-bytes do. S2 compression is on (logs compress
# well). Replay window = max-age (3d default). Beyond that, the S3 archive is the
# ONLY long-term source — everything else is gone after 3 days (accepted design).
#
# TUNABLE LIMITS LIVE IN A CONFIGMAP (nats-stream-limits): max_age, max_bytes,
# dupe_window. Change the ConfigMap and re-sync — this Job re-runs and applies
# the new limits via `nats stream edit` (no manual surgery). The ConfigMap is
# generated with a content-hash suffix (kustomize), so editing it changes both
# the ConfigMap name AND this Job's env reference → the PostSync hook Job's spec
# changes and Argo re-runs it (belt-and-suspenders on top of hooks running each
# sync; hook-delete-policy=BeforeHookCreation recreates it every time).
#
# Consumers (independent offsets = true fan-out):
# transform -> whole log stream, feeds the ClickHouse transform tier
# archiver -> configurable security-relevant subset, feeds the S3 archiver.
# Default filter is Vault audit (logs.k8s.vault.>); ADD subjects
# by editing ARCHIVE_SUBJECTS (space-separated -> repeated
# --filter). Exact default set is an open decision for Ben.
#
# Runbook (replay):
# (a) reprocess from JetStream (within max-age, 3d): scale the transform tier
# to 0, then `nats consumer rm LOGS transform` and re-run this Job
# (recreates at DeliverAll), or `nats consumer edit`/`--replay` from a
# start seq/time.
# (b) long-horizon (beyond JetStream): re-ingest S3 archive objects back
# through the transform tier (vector aws_s3 source or a one-shot Job).
apiVersion: batch/v1
kind: Job
metadata:
name: nats-bootstrap
namespace: logging
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
labels:
app.kubernetes.io/name: nats-bootstrap
app.kubernetes.io/component: bootstrap
spec:
backoffLimit: 20
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app.kubernetes.io/name: nats-bootstrap
vector.dev/exclude: "true"
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
containers:
- name: nats-bootstrap
image: docker.io/natsio/nats-box:0.18.0
# nats CLI stats the working directory when loading its response
# schemas; under readOnlyRootFilesystem + runAsUser 1000 the image's
# default WORKDIR is not accessible ("stat .: permission denied"), so
# run from the writable /tmp emptyDir.
workingDir: /tmp
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
env:
- name: HOME
value: /tmp
- name: NATS_URL
value: "nats://nats.logging.svc.cluster.local:4222"
- name: NATS_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: nats-auth
key: admin_password
# Tunable stream limits — sourced from the ConfigMap.
- name: MAX_AGE
valueFrom:
configMapKeyRef:
name: nats-stream-limits
key: max_age
- name: MAX_BYTES
valueFrom:
configMapKeyRef:
name: nats-stream-limits
key: max_bytes
- name: DUPE_WINDOW
valueFrom:
configMapKeyRef:
name: nats-stream-limits
key: dupe_window
# Space-separated subject filters for the archiver consumer.
- name: ARCHIVE_SUBJECTS
value: "logs.k8s.vault.>"
command:
- /bin/sh
- -ec
- |
export NATS_USER=log-admin NATS_PASSWORD="$NATS_ADMIN_PASSWORD"
echo "Waiting for NATS + JetStream ..."
until nats --server "$NATS_URL" account info >/dev/null 2>&1; do
echo " not ready, retry in 5s"; sleep 5
done
echo "Ensuring stream LOGS (max_age=$MAX_AGE max_bytes=$MAX_BYTES dupe=$DUPE_WINDOW) ..."
# Create if absent; otherwise converge the mutable limits from the
# ConfigMap. (storage/retention/replicas are immutable, set only on
# create.)
nats stream add LOGS \
--subjects='logs.>' --storage=file --replicas=3 \
--retention=limits --discard=old --compression=s2 \
--max-age="$MAX_AGE" --max-bytes="$MAX_BYTES" \
--max-msgs=-1 --max-msgs-per-subject=-1 --max-msg-size=-1 \
--max-consumers=-1 --dupe-window="$DUPE_WINDOW" --defaults 2>/dev/null \
&& echo " created" \
|| nats stream edit -f LOGS \
--subjects='logs.>' --discard=old --compression=s2 \
--max-age="$MAX_AGE" --max-bytes="$MAX_BYTES" \
--max-msgs=-1 --max-msgs-per-subject=-1 --max-msg-size=-1 \
--max-consumers=-1 --dupe-window="$DUPE_WINDOW"
echo "Ensuring consumer transform ..."
nats consumer add LOGS transform \
--pull --filter='logs.>' --deliver=all --ack=explicit \
--max-deliver=-1 --replay=instant --defaults 2>/dev/null \
|| echo " transform already exists"
echo "Ensuring consumer archiver (filters: $ARCHIVE_SUBJECTS) ..."
filter_args=""
for s in $ARCHIVE_SUBJECTS; do filter_args="$filter_args --filter=$s"; done
# shellcheck disable=SC2086
nats consumer add LOGS archiver \
--pull $filter_args --deliver=all --ack=explicit \
--max-deliver=-1 --replay=instant --defaults 2>/dev/null \
|| echo " archiver already exists"
echo "Done."
nats stream info LOGS
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
@@ -0,0 +1,9 @@
---
# Dedicated SA for logarchiver's Vault k8s-auth login (role logging_logarchiver,
# terraform-vault). Only used to fetch the logarchive public key.
apiVersion: v1
kind: ServiceAccount
metadata:
name: logarchiver
namespace: logging
automountServiceAccountToken: false
+73
View File
@@ -0,0 +1,73 @@
---
# ClickHouse credentials for the `vector` user.
#
# Seed the Vault KV entry once (values are NOT stored in git), e.g.:
# PW=$(openssl rand -base64 24)
# HASH=$(printf '%s' "$PW" | sha256sum | cut -d' ' -f1)
# vault kv put kv/kubernetes/namespace/logging/default/clickhouse-credentials \
# username=vector password="$PW" password_sha256_hex="$HASH"
#
# The `logging/default` ServiceAccount reads this path via the templated
# `policies/kv/kubernetes/default.yaml` policy (k8s auth role `default`), so no
# terraform-vault change is required — only the value above must be written.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: clickhouse-credentials
namespace: logging
spec:
destination:
create: true
name: clickhouse-credentials
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/logging/default/clickhouse-credentials
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
---
# ClickHouse credentials for the read-only `logreader` user (CLI tools +
# logviewer UI). Seeded the same way as clickhouse-credentials above:
# PW=$(openssl rand -hex 24)
# HASH=$(printf '%s' "$PW" | sha256sum | cut -d' ' -f1)
# vault kv put kv/kubernetes/namespace/logging/default/clickhouse-logreader \
# username=logreader password="$PW" password_sha256_hex="$HASH"
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: clickhouse-logreader
namespace: logging
spec:
destination:
create: true
name: clickhouse-logreader
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/logging/default/clickhouse-logreader
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
---
# NATS JetStream auth. Distinct passwords for the producer (edge), consumer
# (transform tier + archiver) and admin (bootstrap Job) users. Seed once:
# for k in admin producer consumer; do declare P_$k=$(openssl rand -base64 24); done
# vault kv put kv/kubernetes/namespace/logging/default/nats-auth \
# admin_password="$P_admin" producer_password="$P_producer" consumer_password="$P_consumer"
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: nats-auth
namespace: logging
spec:
destination:
create: true
name: nats-auth
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/logging/default/nats-auth
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
+43
View File
@@ -0,0 +1,43 @@
---
# Vector EDGE agent pipeline (DaemonSet) — thin publisher, single source of
# truth. Mounted via existingConfigMaps (NOT the chart's customConfig, whose
# Helm `tpl` pass collides with Vector's own {{ }} / ${ } syntax). Tails all pod
# logs, attaches only routing tokens, publishes to JetStream. No parsing.
data_dir: /vector-data-dir
api:
enabled: false
sources:
kubernetes_logs:
type: kubernetes_logs
transforms:
# Routing metadata only: NATS-subject-safe namespace + container tokens.
keymeta:
type: remap
inputs:
- kubernetes_logs
source: |
ns = to_string(.kubernetes.pod_namespace || "unknown") ?? "unknown"
.ns_token = replace(ns, r'[^a-zA-Z0-9_-]', "_")
cont = to_string(.kubernetes.container_name || "unknown") ?? "unknown"
.cont_token = replace(cont, r'[^a-zA-Z0-9_-]', "_")
sinks:
to_jetstream:
type: nats
inputs:
- keymeta
url: nats://nats.logging.svc.cluster.local:4222
connection_name: vector-agent
subject: "logs.k8s.{{ ns_token }}.{{ cont_token }}"
jetstream:
enabled: true
auth:
strategy: user_password
user_password:
user: log-producer
password: ${NATS_PRODUCER_PASSWORD}
encoding:
codec: json
@@ -0,0 +1,668 @@
---
# `vector test` unit tests for the aggregator transforms. Merged with
# aggregator.yaml in CI (.woodpecker/vector-test.yaml). This is the pattern the
# per-app parsing follow-ups extend: add a test per new transform here.
tests:
- name: subject_routes_k8s_vs_vm
inputs:
- insert_at: route
type: log
log_fields:
subject: "logs.k8s.shop.web"
message: "routed"
outputs:
- extract_from: route.k8s
conditions:
- type: vrl
source: |
assert_eq!(.message, "routed")
- name: k8s_log_is_normalised
inputs:
- insert_at: k8s_shape
type: log
log_fields:
message: "hello from pod"
stream: "stdout"
timestamp: "2026-07-27T00:00:00Z"
kubernetes.pod_name: "web-abc"
kubernetes.pod_namespace: "shop"
kubernetes.container_name: "web"
kubernetes.pod_node_name: "node-1"
outputs:
- extract_from: k8s_shape
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "shop")
assert_eq!(.pod, "web-abc")
assert_eq!(.container, "web")
assert_eq!(.host, "node-1")
assert_eq!(.stream, "stdout")
assert_eq!(.message, "hello from pod")
- name: vm_log_is_normalised
inputs:
- insert_at: vm_shape
type: log
log_fields:
message: "sshd started"
host: "vm-db-1"
severity: "info"
role: "database"
outputs:
- extract_from: vm_shape
conditions:
- type: vrl
source: |
assert_eq!(.source, "vm")
assert_eq!(.host, "vm-db-1")
assert_eq!(.severity, "info")
assert_eq!(.message, "sshd started")
assert_eq!(.labels.role, "database")
# --- catch-all preservation: an unclaimed k8s event still flows app_route ->
# generic route -> k8s_shape (proves the two-stage chain keeps the fallback) ---
- name: unclaimed_k8s_falls_through_to_generic
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.shop.web"
message: "plain app log"
outputs:
- extract_from: route.k8s
conditions:
- type: vrl
source: |
assert_eq!(.message, "plain app log")
# --- Tier-1: Authentik SSO (k8s, LIVE NOW) ---
- name: authentik_routes_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.authentik.server"
message: "routed"
outputs:
- extract_from: app_route.authentik
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: authentik_parse_extracts_event
inputs:
- insert_at: authentik_parse
type: log
log_fields:
subject: "logs.k8s.authentik.server"
stream: "stdout"
kubernetes.pod_namespace: "authentik"
kubernetes.container_name: "server"
kubernetes.pod_node_name: "node-2"
message: '{"event":"login","action":"login","user":"alice","client_ip":"203.0.113.9","result":"success","level":"info","logger":"authentik.events","timestamp":"2026-07-27T00:00:00Z"}'
outputs:
- extract_from: authentik_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "authentik")
assert_eq!(.container, "server")
assert_eq!(.severity, "info")
assert_eq!(.message, "login")
assert_eq!(.labels.app, "authentik")
assert_eq!(.fields.event, "login")
assert_eq!(.fields.action, "login")
assert_eq!(.fields.user, "alice")
assert_eq!(.fields.client_ip, "203.0.113.9")
assert_eq!(.fields.result, "success")
# --- Tier-1: Traefik ingress (k8s, JSON access logs) ---
- name: traefik_routes_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.traefik-system.traefik"
message: "routed"
outputs:
- extract_from: app_route.traefik
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: traefik_parse_extracts_access_fields
inputs:
- insert_at: traefik_parse
type: log
log_fields:
subject: "logs.k8s.traefik-system.traefik"
kubernetes.pod_namespace: "traefik-system"
kubernetes.container_name: "traefik"
kubernetes.pod_node_name: "node-3"
message: '{"RouterName":"web@kubernetes","ServiceName":"shop-svc@kubernetes","RequestMethod":"GET","RequestPath":"/api","RequestHost":"shop.example.net","RequestProtocol":"HTTP/1.1","DownstreamStatus":200,"Duration":5000000,"ClientHost":"203.0.113.5","StartUTC":"2026-07-27T00:00:00Z"}'
outputs:
- extract_from: traefik_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "traefik-system")
assert_eq!(.labels.app, "traefik")
assert_eq!(.message, "GET /api 200")
assert_eq!(.fields.route, "web@kubernetes")
assert_eq!(.fields.service, "shop-svc@kubernetes")
assert_eq!(.fields.method, "GET")
assert_eq!(.fields.path, "/api")
assert_eq!(.fields.host, "shop.example.net")
assert_eq!(.fields.status, "200")
assert_eq!(.fields.duration_ms, "5")
assert_eq!(.fields.client_ip, "203.0.113.5")
# --- Tier-1: Vault/OpenBao file audit (VM, awaiting VM vector) ---
- name: vault_routes_by_file
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.vault1_syd1"
file: "/var/log/vault_audit.log"
message: "routed"
outputs:
- extract_from: app_route.vault
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: vault_parse_extracts_request
inputs:
- insert_at: vault_parse
type: log
log_fields:
subject: "logs.vm.vault1_syd1"
host: "vault1"
file: "/var/log/vault_audit.log"
message: '{"time":"2026-07-27T00:00:00Z","type":"response","auth":{"display_name":"token"},"request":{"operation":"read","path":"secret/data/app","remote_address":"10.0.0.9"},"error":""}'
outputs:
- extract_from: vault_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "vm")
assert_eq!(.host, "vault1")
assert_eq!(.labels.app, "vault")
assert_eq!(.message, "read secret/data/app")
assert_eq!(.fields.type, "response")
assert_eq!(.fields.display_name, "token")
assert_eq!(.fields.operation, "read")
assert_eq!(.fields.path, "secret/data/app")
assert_eq!(.fields.remote_address, "10.0.0.9")
# --- Tier-1: nginx access (VM, awaiting VM vector) ---
- name: nginx_access_routes_by_file
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.web1_syd1"
file: "/var/log/nginx/shop_access.log"
message: "routed"
outputs:
- extract_from: app_route.nginx_access
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: nginx_access_parse_extracts_combined
inputs:
- insert_at: nginx_access_parse
type: log
log_fields:
subject: "logs.vm.web1_syd1"
host: "web1"
file: "/var/log/nginx/shop_access.log"
message: '192.0.2.10 - - [27/Jul/2026:00:00:00 +0000] "GET /index.html HTTP/1.1" 200 1024 "https://ref.example/" "Mozilla/5.0" 0.012'
outputs:
- extract_from: nginx_access_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "vm")
assert_eq!(.stream, "access")
assert_eq!(.labels.log_type, "access")
assert_eq!(.fields.client_ip, "192.0.2.10")
assert_eq!(.fields.method, "GET")
assert_eq!(.fields.path, "/index.html")
assert_eq!(.fields.status, "200")
assert_eq!(.fields.bytes, "1024")
assert_eq!(.fields.referer, "https://ref.example/")
assert_eq!(.fields.user_agent, "Mozilla/5.0")
assert_eq!(.fields.request_time, "0.012")
# --- Tier-1: nginx error (VM, awaiting VM vector) ---
- name: nginx_error_routes_by_file
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.web1_syd1"
file: "/var/log/nginx/shop_error.log"
message: "routed"
outputs:
- extract_from: app_route.nginx_error
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: nginx_error_parse_extracts_fields
inputs:
- insert_at: nginx_error_parse
type: log
log_fields:
subject: "logs.vm.web1_syd1"
host: "web1"
file: "/var/log/nginx/shop_error.log"
message: '2026/07/27 00:00:00 [error] 1234#0: *5 open() "/var/www/x" failed (2: No such file or directory), client: 192.0.2.20, server: shop, request: "GET / HTTP/1.1", host: "shop"'
outputs:
- extract_from: nginx_error_parse
conditions:
- type: vrl
source: |
assert_eq!(.stream, "error")
assert_eq!(.severity, "error")
assert_eq!(.labels.log_type, "error")
assert_eq!(.fields.level, "error")
assert_eq!(.fields.pid, "1234")
assert_eq!(.fields.cid, "5")
assert_eq!(.fields.client_ip, "192.0.2.20")
# --- Tier-1: HAProxy httplog (VM journald, awaiting VM vector) ---
- name: haproxy_routes_by_identifier
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.halb1_syd1"
SYSLOG_IDENTIFIER: "haproxy"
message: "routed"
outputs:
- extract_from: app_route.haproxy
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: haproxy_parse_extracts_timers
inputs:
- insert_at: haproxy_parse
type: log
log_fields:
subject: "logs.vm.halb1_syd1"
host: "halb1"
SYSLOG_IDENTIFIER: "haproxy"
message: '192.0.2.30:54321 [27/Jul/2026:00:00:00.123] fe_http be_app/app1 10/0/1/2/13 200 512 - - ---- 5/4/3/2/0 0/0 "GET /health HTTP/1.1"'
outputs:
- extract_from: haproxy_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "vm")
assert_eq!(.labels.app, "haproxy")
assert_eq!(.fields.client_ip, "192.0.2.30")
assert_eq!(.fields.frontend, "fe_http")
assert_eq!(.fields.backend, "be_app")
assert_eq!(.fields.server, "app1")
assert_eq!(.fields.tq, "10")
assert_eq!(.fields.tw, "0")
assert_eq!(.fields.tc, "1")
assert_eq!(.fields.tr, "2")
assert_eq!(.fields.tt, "13")
assert_eq!(.fields.termination_state, "----")
assert_eq!(.fields.retries, "0")
assert_eq!(.fields.status, "200")
assert_eq!(.fields.bytes, "512")
# --- Tier-1: glauth LDAP (VM, awaiting VM vector) ---
- name: glauth_routes_by_identifier
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.ldap1_syd1"
SYSLOG_IDENTIFIER: "glauth"
message: "routed"
outputs:
- extract_from: app_route.glauth
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: glauth_parse_extracts_bind
inputs:
- insert_at: glauth_parse
type: log
log_fields:
subject: "logs.vm.ldap1_syd1"
host: "ldap1"
SYSLOG_IDENTIFIER: "glauth"
message: '{"level":"info","msg":"Bind success as user","bindDN":"cn=admin,dc=example,dc=com","src":"192.0.2.40:1234","time":"2026-07-27T00:00:00Z"}'
outputs:
- extract_from: glauth_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "vm")
assert_eq!(.host, "ldap1")
assert_eq!(.severity, "info")
assert_eq!(.labels.app, "glauth")
assert_eq!(.fields.bindDN, "cn=admin,dc=example,dc=com")
assert_eq!(.fields.remote, "192.0.2.40:1234")
assert_eq!(.fields.success, "true")
# ================= Tier-2 (stacks on #318) =================
# --- BIND query logs (k8s bind-* + VM named) ---
- name: bind_routes_k8s_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.bind-internal.named"
message: "routed"
outputs:
- extract_from: app_route.bind_query
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: bind_routes_vm_by_identifier
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.dns1_syd1"
SYSLOG_IDENTIFIER: "named"
message: "routed"
outputs:
- extract_from: app_route.bind_query
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: bind_parse_extracts_query
inputs:
- insert_at: bind_query_parse
type: log
log_fields:
subject: "logs.k8s.bind-internal.named"
kubernetes.pod_namespace: "bind-internal"
kubernetes.container_name: "named"
kubernetes.pod_node_name: "node-4"
message: '02-Aug-2026 00:00:00.123 client @0x7f 192.0.2.1#40426 (www.example.com): view internal: query: www.example.com IN A +E(0)K (198.18.200.7)'
outputs:
- extract_from: bind_query_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "bind-internal")
assert_eq!(.labels.app, "bind")
assert_eq!(.message, "query www.example.com A")
assert_eq!(.fields.client_ip, "192.0.2.1")
assert_eq!(.fields.qname, "www.example.com")
assert_eq!(.fields.qclass, "IN")
assert_eq!(.fields.qtype, "A")
assert_eq!(.fields.view, "internal")
# --- Rancher audit (k8s, cattle-system sidecar) ---
- name: rancher_routes_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.cattle-system.rancher-audit-log"
message: "routed"
outputs:
- extract_from: app_route.rancher_audit
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: rancher_parse_extracts_audit
inputs:
- insert_at: rancher_audit_parse
type: log
log_fields:
subject: "logs.k8s.cattle-system.rancher-audit-log"
kubernetes.pod_namespace: "cattle-system"
kubernetes.container_name: "rancher-audit-log"
kubernetes.pod_node_name: "node-5"
message: '{"auditID":"abc-123","requestURI":"/v3/tokens","user":{"name":"u-alice","group":["admins"]},"method":"GET","remoteAddr":"10.42.0.9:1234","responseCode":200,"requestTimestamp":"2026-08-01T00:00:00Z"}'
outputs:
- extract_from: rancher_audit_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "cattle-system")
assert_eq!(.labels.app, "rancher")
assert_eq!(.labels.log_type, "audit")
assert_eq!(.message, "GET /v3/tokens 200")
assert_eq!(.fields.user, "u-alice")
assert_eq!(.fields.verb, "GET")
assert_eq!(.fields.uri, "/v3/tokens")
assert_eq!(.fields.status, "200")
# --- CNPG Postgres (ONE transform, all clusters) ---
- name: cnpg_routes_by_postgres_container
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.litellm.postgres"
message: "routed"
outputs:
- extract_from: app_route.cnpg_pg
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
# mutual exclusivity: an app-namespace CNPG pod (authentik) is claimed by
# cnpg_pg, NOT the authentik app route (which now carves out .postgres).
- name: cnpg_authentik_postgres_routes_to_cnpg
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.authentik.postgres"
message: "routed"
outputs:
- extract_from: app_route.cnpg_pg
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: cnpg_parse_extracts_record
inputs:
- insert_at: cnpg_pg_parse
type: log
log_fields:
subject: "logs.k8s.litellm.postgres"
kubernetes.pod_namespace: "litellm"
kubernetes.container_name: "postgres"
kubernetes.pod_node_name: "node-6"
kubernetes.pod_labels."cnpg.io/cluster": "litellm-postgres"
message: '{"level":"info","ts":"2026-08-01T00:00:00Z","logger":"postgres","msg":"record","record":{"user_name":"litellm","database_name":"litellm","error_severity":"LOG","message":"duration: 12.345 ms statement: SELECT 1","query":""}}'
outputs:
- extract_from: cnpg_pg_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "litellm")
assert_eq!(.severity, "LOG")
assert_eq!(.labels.app, "cnpg")
assert_eq!(.labels.cluster, "litellm-postgres")
assert_eq!(.fields.error_severity, "LOG")
assert_eq!(.fields.duration_ms, "12.345")
assert_eq!(.fields.user, "litellm")
assert_eq!(.fields.database, "litellm")
# --- Gitea router/access (k8s + VM) ---
- name: gitea_routes_k8s_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.gitea.gitea"
message: "routed"
outputs:
- extract_from: app_route.gitea
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: gitea_parse_router_line
inputs:
- insert_at: gitea_parse
type: log
log_fields:
subject: "logs.k8s.gitea.gitea"
kubernetes.pod_namespace: "gitea"
kubernetes.container_name: "gitea"
kubernetes.pod_node_name: "node-7"
message: '2026/08/01 00:00:00 .../router.go:100:func() [I] router: completed GET /user/login for 10.0.0.1:0, 200 OK in 12.3ms @ web/base.go:1'
outputs:
- extract_from: gitea_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "gitea")
assert_eq!(.labels.app, "gitea")
assert_eq!(.message, "GET /user/login 200")
assert_eq!(.fields.method, "GET")
assert_eq!(.fields.path, "/user/login")
assert_eq!(.fields.status, "200")
assert_eq!(.fields.latency, "12.3ms")
- name: gitea_parse_access_line
inputs:
- insert_at: gitea_parse
type: log
log_fields:
subject: "logs.k8s.gitea.gitea"
kubernetes.pod_namespace: "gitea"
kubernetes.container_name: "gitea"
message: '10.0.0.5 - alice [01/Aug/2026:00:00:00 +0000] "POST /repo/foo HTTP/1.1" 201 512 "-" "git/2.0"'
outputs:
- extract_from: gitea_parse
conditions:
- type: vrl
source: |
assert_eq!(.fields.method, "POST")
assert_eq!(.fields.path, "/repo/foo")
assert_eq!(.fields.status, "201")
assert_eq!(.fields.user, "alice")
assert_eq!(.fields.client_ip, "10.0.0.5")
# --- PuppetServer / PuppetDB (k8s stdout) ---
- name: puppet_routes_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.puppet.puppetserver"
message: "routed"
outputs:
- extract_from: app_route.puppet
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: puppet_parse_logback_line
inputs:
- insert_at: puppet_parse
type: log
log_fields:
subject: "logs.k8s.puppet.puppetserver"
kubernetes.pod_namespace: "puppet"
kubernetes.container_name: "puppetserver"
kubernetes.pod_node_name: "node-8"
message: '2026-08-01 00:00:00,123 INFO [qtp123-45] [puppetserver] Compiled catalog for web01.unkin.net in environment production in 1.23 seconds'
outputs:
- extract_from: puppet_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "puppet")
assert_eq!(.severity, "INFO")
assert_eq!(.labels.app, "puppet")
assert_eq!(.fields.level, "INFO")
assert_eq!(.fields.logger, "puppetserver")
assert_eq!(.fields.node, "web01.unkin.net")
# --- LiteLLM request logs (k8s JSON) ---
- name: litellm_routes_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.litellm.litellm"
message: "routed"
outputs:
- extract_from: app_route.litellm
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: litellm_parse_extracts_request
inputs:
- insert_at: litellm_parse
type: log
log_fields:
subject: "logs.k8s.litellm.litellm"
kubernetes.pod_namespace: "litellm"
kubernetes.container_name: "litellm"
kubernetes.pod_node_name: "node-9"
message: '{"message":"Request completed","level":"info","model":"gpt-4o","total_tokens":1234,"response_time":0.532,"api_key":"sk-abc","status":"success","timestamp":"2026-08-01T00:00:00Z"}'
outputs:
- extract_from: litellm_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "litellm")
assert_eq!(.severity, "info")
assert_eq!(.message, "Request completed")
assert_eq!(.labels.app, "litellm")
assert_eq!(.fields.model, "gpt-4o")
assert_eq!(.fields.tokens, "1234")
assert_eq!(.fields.latency, "0.532")
assert_eq!(.fields.key, "sk-abc")
assert_eq!(.fields.status, "success")
# --- Postfix maillog (VM, per-line best-effort) ---
- name: postfix_routes_by_identifier
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.mail1_syd1"
SYSLOG_IDENTIFIER: "postfix/qmgr"
message: "routed"
outputs:
- extract_from: app_route.postfix
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: postfix_parse_extracts_line
inputs:
- insert_at: postfix_parse
type: log
log_fields:
subject: "logs.vm.mail1_syd1"
host: "mail1"
SYSLOG_IDENTIFIER: "postfix/smtp"
message: 'ABC123DEF: to=<rcpt@example.com>, relay=mx.example.com[1.2.3.4]:25, delay=1.2, delays=0.1/0/0.5/0.6, dsn=2.0.0, status=sent (250 OK)'
outputs:
- extract_from: postfix_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "vm")
assert_eq!(.host, "mail1")
assert_eq!(.labels.app, "postfix")
assert_eq!(.fields.qid, "ABC123DEF")
assert_eq!(.fields.to, "rcpt@example.com")
assert_eq!(.fields.relay, "mx.example.com[1.2.3.4]:25")
assert_eq!(.fields.delay, "1.2")
assert_eq!(.fields.status, "sent")
assert_eq!(.fields.program, "postfix/smtp")

Some files were not shown because too many files have changed in this diff Show More