From 08a3493097ba75e335943beca91691b107f9beff Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 23 Aug 2026 18:51:15 +1000 Subject: [PATCH] logging: deploy logviewer v0.1.0 behind oauth2-proxy (#404) ## Why The logviewer UI gives browser access to the ClickHouse log store (`logreader` user). It must only be reachable by authorized users, so it sits behind an Authentik-gated oauth2-proxy front at https://logviewer.unkin.net, mirroring the arrstack front-door and mediamover/pdbmux gateway patterns. ## Changes - Adds the logviewer Deployment (1 replica, `docker-internal/logviewer:v0.1.0`) reading ClickHouse at `clickhouse-logs` via the existing `clickhouse-logreader` Secret, `/healthz` probes, nonroot securityContext - Adds an oauth2-proxy (quay.io/oauth2-proxy/oauth2-proxy:v7.15.3) front with issuer `https://identity.unkin.net/application/o/logviewer/`, `ak_groups` claim, allowed group `akP-logviewer-admin`, internal-CA trust via the combine-certs initContainer - Syncs kv `kubernetes/namespace/logging/default/oauth-credentials` into the `logviewer-oauth-credentials` Secret via the existing `default` VaultAuth (kv path already seeded; no terraform-vault change needed) - Adds ClusterIP Services for the app and the oauth2-proxy front door - Adds internal Gateway + HTTPRoutes for `logviewer.unkin.net` (vault-issuer cert, external-dns -> 198.18.200.4, http->https redirect) - Registers `logviewer/` in the logging base kustomization ## Dependency Login requires the Authentik `logviewer` provider/application from terraform-authentik PR #21. This PR is safe to merge first: the VSO secret sync and the deployment come up regardless; OIDC login only works once #21 is applied. Validated with `kustomize build` (base + au-syd1 overlay) and kubeconform (49/49 valid). Reviewed-on: https://git.unkin.net/unkin/argocd-apps/pulls/404 Co-authored-by: unkin-agent Co-committed-by: unkin-agent --- apps/base/logging/kustomization.yaml | 1 + apps/base/logging/logviewer/deployment.yaml | 80 +++++++++++ apps/base/logging/logviewer/gateway.yaml | 38 +++++ apps/base/logging/logviewer/httproute.yaml | 49 +++++++ .../base/logging/logviewer/kustomization.yaml | 12 ++ .../logviewer/oauth2-proxy-configmap.yaml | 32 +++++ .../logviewer/oauth2-proxy-deployment.yaml | 132 ++++++++++++++++++ apps/base/logging/logviewer/service.yaml | 36 +++++ .../logging/logviewer/vaultstaticsecret.yaml | 21 +++ 9 files changed, 401 insertions(+) create mode 100644 apps/base/logging/logviewer/deployment.yaml create mode 100644 apps/base/logging/logviewer/gateway.yaml create mode 100644 apps/base/logging/logviewer/httproute.yaml create mode 100644 apps/base/logging/logviewer/kustomization.yaml create mode 100644 apps/base/logging/logviewer/oauth2-proxy-configmap.yaml create mode 100644 apps/base/logging/logviewer/oauth2-proxy-deployment.yaml create mode 100644 apps/base/logging/logviewer/service.yaml create mode 100644 apps/base/logging/logviewer/vaultstaticsecret.yaml diff --git a/apps/base/logging/kustomization.yaml b/apps/base/logging/kustomization.yaml index a51e19a..2464c25 100644 --- a/apps/base/logging/kustomization.yaml +++ b/apps/base/logging/kustomization.yaml @@ -15,6 +15,7 @@ resources: - 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`. diff --git a/apps/base/logging/logviewer/deployment.yaml b/apps/base/logging/logviewer/deployment.yaml new file mode 100644 index 0000000..c0394d4 --- /dev/null +++ b/apps/base/logging/logviewer/deployment.yaml @@ -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 diff --git a/apps/base/logging/logviewer/gateway.yaml b/apps/base/logging/logviewer/gateway.yaml new file mode 100644 index 0000000..fbac5bc --- /dev/null +++ b/apps/base/logging/logviewer/gateway.yaml @@ -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 diff --git a/apps/base/logging/logviewer/httproute.yaml b/apps/base/logging/logviewer/httproute.yaml new file mode 100644 index 0000000..efe4464 --- /dev/null +++ b/apps/base/logging/logviewer/httproute.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: logviewer-http-redirect + namespace: logging +spec: + hostnames: + - logviewer.unkin.net + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: logviewer + 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: logviewer + namespace: logging +spec: + hostnames: + - logviewer.unkin.net + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: logviewer + sectionName: https + rules: + - backendRefs: + - group: "" + kind: Service + name: logviewer-oauth2 + port: 80 + weight: 1 + matches: + - path: + type: PathPrefix + value: / diff --git a/apps/base/logging/logviewer/kustomization.yaml b/apps/base/logging/logviewer/kustomization.yaml new file mode 100644 index 0000000..2189b2e --- /dev/null +++ b/apps/base/logging/logviewer/kustomization.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - vaultstaticsecret.yaml + - deployment.yaml + - oauth2-proxy-configmap.yaml + - oauth2-proxy-deployment.yaml + - service.yaml + - gateway.yaml + - httproute.yaml diff --git a/apps/base/logging/logviewer/oauth2-proxy-configmap.yaml b/apps/base/logging/logviewer/oauth2-proxy-configmap.yaml new file mode 100644 index 0000000..9f5631e --- /dev/null +++ b/apps/base/logging/logviewer/oauth2-proxy-configmap.yaml @@ -0,0 +1,32 @@ +--- +# 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: logviewer-oauth2-env + namespace: logging +data: + OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180" + OAUTH2_PROXY_PROVIDER: "oidc" + # 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-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: "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" + OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true" diff --git a/apps/base/logging/logviewer/oauth2-proxy-deployment.yaml b/apps/base/logging/logviewer/oauth2-proxy-deployment.yaml new file mode 100644 index 0000000..adfc739 --- /dev/null +++ b/apps/base/logging/logviewer/oauth2-proxy-deployment.yaml @@ -0,0 +1,132 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: logviewer-oauth2 + namespace: logging + annotations: + configmap.reloader.stakater.com/auto: "true" + secret.reloader.stakater.com/reload: "logviewer-oauth-credentials,vault-ca-cert" +spec: + replicas: 1 + selector: + matchLabels: + app: logviewer-oauth2 + strategy: + rollingUpdate: + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: logviewer-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: artifactapi.k8s.syd1.au.unkin.net/dockerhub/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: logviewer-oauth2-env + optional: false + env: + - name: OAUTH2_PROXY_CLIENT_ID + valueFrom: + secretKeyRef: + name: logviewer-oauth-credentials + key: client_id + - name: OAUTH2_PROXY_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: logviewer-oauth-credentials + key: client_secret + - name: OAUTH2_PROXY_COOKIE_SECRET + valueFrom: + secretKeyRef: + name: logviewer-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 diff --git a/apps/base/logging/logviewer/service.yaml b/apps/base/logging/logviewer/service.yaml new file mode 100644 index 0000000..dfc72a4 --- /dev/null +++ b/apps/base/logging/logviewer/service.yaml @@ -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 diff --git a/apps/base/logging/logviewer/vaultstaticsecret.yaml b/apps/base/logging/logviewer/vaultstaticsecret.yaml new file mode 100644 index 0000000..47ad5bb --- /dev/null +++ b/apps/base/logging/logviewer/vaultstaticsecret.yaml @@ -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