Compare commits

..

1 Commits

Author SHA1 Message Date
unkin-agent d4a39c44e6 add agent-observability service account and namespace-scoped RBAC
ci/woodpecker/pr/vector-test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
2026-09-27 00:38:38 +10:00
24 changed files with 320 additions and 346 deletions
@@ -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 @@
---
# In-zone address for the nameserver names acme.unkin.net publishes. Without it
# a reseed glues ns1.acme.unkin.net to the primary pod IP, which is unroutable
# off-cluster and goes stale on reschedule. The apex NS RRset itself comes from
# BindZone.spec.nameservers. DNSRecords must live in the same namespace as their
# BindZone.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- a
+1 -1
View File
@@ -7,5 +7,5 @@ resources:
- cluster.yaml
- tsigkey.yaml
- zones.yaml
- acme-unkin-net
- records.yaml
- agent-dns-rolebinding.yaml
+36
View File
@@ -0,0 +1,36 @@
# 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: 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.
---
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
@@ -162,18 +162,3 @@ spec:
# traefik-EXTERNAL (DMZ) gateway VIP; the watchstate-external Gateway serves
# the watchstate.unkin.net front door (oauth2-proxy) there.
- 198.18.199.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
@@ -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,logging"
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "cheeztv,arrstack,authentik,gitea,watchstate,mediamark,repospawner,haproxy"
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,logging"
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "cheeztv,arrstack,authentik,gitea,watchstate,mediamark,repospawner,haproxy"
privateKey:
size: 4096
dnsNames:
@@ -0,0 +1,16 @@
---
# Confines the agent-observability service account (in vm-system) to the
# agent-observability ClusterRole within this namespace.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: agent-observability
namespace: logging
subjects:
- kind: ServiceAccount
name: agent-observability
namespace: vm-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: agent-observability
+1 -1
View File
@@ -17,7 +17,7 @@ resources:
- configmap_logarchiver.yaml
- deployment_logarchiver.yaml
- logviewer
- vlogs
- agent-observability-rolebinding.yaml
# Vector pipelines are the single source of truth (also validated by
# `vector test` in CI). Mounted into each tier via `existingConfigMaps`.
-38
View File
@@ -1,38 +0,0 @@
---
# External (DMZ) front for the VictoriaLogs UI on vlogs.unkin.net via the
# external Traefik (LB VIP 198.18.199.0). TLS terminates with the real Let's
# Encrypt *.unkin.net wildcard (Certificate wildcard-unkin-net in cert-manager,
# reflected into this namespace as wildcard-unkin-net-tls by the emberstack
# reflector), so there is no cert-manager annotation here. The apex
# vlogs.unkin.net A record lives in the bind-operator unkin.net zone, NOT
# external-dns, so no external-dns annotation either. oauth2-proxy fronts it.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
labels:
traefik.io/instance: external
name: vlogs-external
namespace: logging
spec:
gatewayClassName: traefik-external
listeners:
- allowedRoutes:
namespaces:
from: Same
hostname: vlogs.unkin.net
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
hostname: vlogs.unkin.net
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: wildcard-unkin-net-tls
mode: Terminate
-49
View File
@@ -1,49 +0,0 @@
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: vlogs-http-redirect
namespace: logging
spec:
hostnames:
- vlogs.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: vlogs-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: vlogs
namespace: logging
spec:
hostnames:
- vlogs.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: vlogs-external
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: vlogs-oauth2
port: 80
weight: 1
matches:
- path:
type: PathPrefix
value: /
@@ -1,11 +0,0 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- vaultstaticsecret.yaml
- oauth2-proxy-configmap.yaml
- oauth2-proxy-deployment.yaml
- service.yaml
- gateway.yaml
- httproute.yaml
@@ -1,28 +0,0 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: vlogs-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/"
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_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_REVERSE_PROXY: "true"
OAUTH2_PROXY_PROVIDER_CA_FILES: "/etc/ssl/combined/ca-certificates.crt"
OAUTH2_PROXY_CODE_CHALLENGE_METHOD: "S256"
OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true"
@@ -1,132 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: vlogs-oauth2
namespace: logging
annotations:
configmap.reloader.stakater.com/auto: "true"
secret.reloader.stakater.com/reload: "vlogs-oauth-credentials,vault-ca-cert"
spec:
replicas: 1
selector:
matchLabels:
app: vlogs-oauth2
strategy:
rollingUpdate:
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
labels:
app: vlogs-oauth2
spec:
serviceAccountName: default
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
initContainers:
# identity.unkin.net serves a Vault-PKI cert; combine the system roots
# with the internal CA so oauth2-proxy's OIDC HTTP client trusts it.
- name: combine-certs
image: docker.io/library/alpine:3
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- cat /etc/ssl/certs/ca-certificates.crt /custom-ca/ca.crt > /combined-certs/ca-certificates.crt
volumeMounts:
- name: vault-ca-cert
mountPath: /custom-ca
readOnly: true
- name: combined-certs
mountPath: /combined-certs
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 64Mi
containers:
- name: oauth2-proxy
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3
imagePullPolicy: IfNotPresent
ports:
- containerPort: 4180
name: http
protocol: TCP
envFrom:
- configMapRef:
name: vlogs-oauth2-env
optional: false
env:
- name: OAUTH2_PROXY_CLIENT_ID
valueFrom:
secretKeyRef:
name: vlogs-oauth-credentials
key: client_id
- name: OAUTH2_PROXY_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: vlogs-oauth-credentials
key: client_secret
- name: OAUTH2_PROXY_COOKIE_SECRET
valueFrom:
secretKeyRef:
name: vlogs-oauth-credentials
key: cookie_secret
volumeMounts:
- name: combined-certs
mountPath: /etc/ssl/combined
readOnly: true
livenessProbe:
httpGet:
path: /ping
port: http
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
volumes:
- name: vault-ca-cert
secret:
secretName: vault-ca-cert
items:
- key: ca.crt
path: ca.crt
- name: combined-certs
emptyDir: {}
restartPolicy: Always
-19
View File
@@ -1,19 +0,0 @@
---
# Front-door entry Service: the HTTPRoute for vlogs.unkin.net targets this, so
# all traffic enters via oauth2-proxy.
apiVersion: v1
kind: Service
metadata:
name: vlogs-oauth2
namespace: logging
spec:
internalTrafficPolicy: Cluster
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
selector:
app: vlogs-oauth2
sessionAffinity: None
type: ClusterIP
@@ -1,17 +0,0 @@
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: vlogs-oauth-credentials
namespace: logging
spec:
destination:
create: true
name: vlogs-oauth-credentials
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/logging/default/vlogs-oauth-credentials
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
@@ -0,0 +1,16 @@
---
# Confines the agent-observability service account (in vm-system) to the
# agent-observability ClusterRole within this namespace.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: agent-observability
namespace: observability
subjects:
- kind: ServiceAccount
name: agent-observability
namespace: vm-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: agent-observability
@@ -13,3 +13,4 @@ resources:
- httproute.yaml
- vmpodscrape-cnpg.yaml
- vmpodscrape-traefik.yaml
- agent-observability-rolebinding.yaml
@@ -0,0 +1,50 @@
---
# Static service account that Vault's kubernetes secret engine mints scoped
# tokens for (agent-observability role). RBAC is confined to the metrics and
# logging namespaces via the per-namespace RoleBindings, not a
# ClusterRoleBinding. Workloads and VictoriaMetrics CRs are deliberately
# patch/update only: deleting a VMCluster or VLCluster destroys data.
apiVersion: v1
kind: ServiceAccount
metadata:
name: agent-observability
namespace: vm-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: agent-observability
rules:
- apiGroups: ["operator.victoriametrics.com"]
resources: ["*"]
verbs: ["get", "list", "watch", "create", "patch", "update"]
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets", "daemonsets"]
verbs: ["get", "list", "watch", "patch", "update"]
# delete permits a rolling restart without granting workload deletion.
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch", "delete"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
- apiGroups: [""]
resources: ["services", "configmaps", "endpoints", "events"]
verbs: ["get", "list", "watch"]
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["gateways", "httproutes"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: agent-observability
namespace: vm-system
subjects:
- kind: ServiceAccount
name: agent-observability
namespace: vm-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: agent-observability
+1
View File
@@ -4,3 +4,4 @@ kind: Kustomization
resources:
- namespace.yaml
- agent-observability-rbac.yaml
@@ -0,0 +1,39 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: gocache-nginx
namespace: woodpecker
labels:
app.kubernetes.io/name: gocache
app.kubernetes.io/component: proxy
data:
nginx.conf: |
worker_processes auto;
error_log /dev/stderr warn;
pid /tmp/nginx.pid;
events {
worker_connections 512;
}
# GOCACHEPROG is a raw byte stream, not HTTP, so this must be stream{} not http{}.
stream {
server {
listen 9090;
# The protocol has no authentication: anyone who can reach this port can
# write cache entries, which become code in every build that reads them.
# Loopback is the kubectl port-forward fallback; in a pod netns it is
# only these two containers.
allow 127.0.0.1/32;
allow 10.10.12.200/32;
allow 10.42.0.0/16;
deny all;
# A connect session lasts the whole build; the 10m default cuts long builds off.
proxy_timeout 2h;
proxy_connect_timeout 5s;
proxy_pass 127.0.0.1:9080;
}
}
@@ -0,0 +1,131 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gocache
namespace: woodpecker
annotations:
configmap.reloader.stakater.com/reload: "gocache-nginx"
secret.reloader.stakater.com/reload: "gocache-s3,vault-ca-cert"
labels:
app.kubernetes.io/name: gocache
app.kubernetes.io/component: cache
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: gocache
template:
metadata:
labels:
app.kubernetes.io/name: gocache
app.kubernetes.io/component: cache
spec:
serviceAccountName: default
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: go-cache-plugin
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/go-cache-plugin:v0.1.0
imagePullPolicy: IfNotPresent
# Root flags must precede the subcommand; only --plugin belongs to serve.
args:
- --cache-dir=/var/cache/gocache
- --bucket=gocache
# Explicit region skips the GetBucketLocation probe, which RGW handles poorly.
- --region=us-east-1
- --s3-endpoint-url=https://s3.ceph.unkin.net
- --s3-path-style
- serve
- --plugin=9080
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: gocache-s3
key: AWS_ACCESS_KEY_ID
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: gocache-s3
key: AWS_SECRET_ACCESS_KEY
# s3.ceph.unkin.net is served by the estate CA, not a public root.
- name: AWS_CA_BUNDLE
value: /etc/ssl/vault-ca/ca.crt
volumeMounts:
- name: cache
mountPath: /var/cache/gocache
- name: vault-ca
mountPath: /etc/ssl/vault-ca
readOnly: true
securityContext:
runAsUser: 65532
runAsGroup: 65532
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "2"
memory: 2Gi
- name: nginx
image: docker.io/nginx:1.29.8-alpine
imagePullPolicy: IfNotPresent
# Bypass the image entrypoint: its config scripts write to a read-only rootfs.
command:
- nginx
- -g
- daemon off;
ports:
- containerPort: 9090
name: gocache
protocol: TCP
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
- name: tmp
mountPath: /tmp
securityContext:
runAsUser: 101
runAsGroup: 101
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 500m
memory: 128Mi
volumes:
# Staging cache in front of S3: losing it costs a repopulate, not data.
- name: cache
emptyDir:
sizeLimit: 20Gi
- name: nginx-config
configMap:
name: gocache-nginx
- name: tmp
emptyDir: {}
- name: vault-ca
secret:
secretName: vault-ca-cert
items:
- key: ca.crt
path: ca.crt
+3
View File
@@ -8,6 +8,9 @@ resources:
- cnpg_backup.yaml
- cnpg_pooler.yaml
- gocache_bucket.yaml
- configmap_gocache-nginx.yaml
- deployment_gocache.yaml
- service_gocache.yaml
- serviceaccount_arrproxy_ci.yaml
- serviceaccount_autobackup_operator_ci.yaml
- serviceaccount_ghp.yaml
+23
View File
@@ -0,0 +1,23 @@
---
apiVersion: v1
kind: Service
metadata:
name: gocache
namespace: woodpecker
annotations:
purelb.io/addresses: 198.18.200.11
purelb.io/service-group: common
labels:
app.kubernetes.io/name: gocache
spec:
type: LoadBalancer
# Cluster SNATs off-node traffic to a node address, which would defeat the
# nginx allow rules; Local preserves the wireguard client IP.
externalTrafficPolicy: Local
selector:
app.kubernetes.io/name: gocache
ports:
- name: gocache
port: 9090
targetPort: gocache
protocol: TCP