monitoring: GET-only Loki read gateway for the paperclip read SA (REN-621) #1119

Merged
Ghost merged 1 commits from atlas/ren-621-loki-read-gateway into master 2026-10-04 12:22:00 +00:00
4 changed files with 174 additions and 2 deletions
@@ -10,6 +10,7 @@ resources:
- loki-basic-auth-externalsecret.yaml
- loki-basic-auth-middleware.yaml
- loki-ingress.yaml
- loki-read-gateway.yaml
- alertmanager-gate-service.yaml
- alertmanager-gate-ingress.yaml
- prometheus-gate-service.yaml
@@ -18,6 +19,13 @@ resources:
- smartctl-exporter.yaml
- intel-gpu-exporter.yaml
- dashboards/
configMapGenerator:
# Hash suffix kept on purpose: an edit to the .conf rolls the gateway pod,
# which is stateless and off the ingest path.
- name: loki-read-gateway
namespace: monitoring
files:
- loki-read-gateway.conf
# prometheusrule-garage.yaml lives in ./monitoring-rules/ and is applied by a
# separate Flux Kustomization that dependsOn `monitoring` — otherwise the
# server-side dry-run of the PrometheusRule fails on a fresh cluster
@@ -0,0 +1,60 @@
# GET-only allow-list in front of Loki (REN-621, owner decision on REN-622).
#
# Loki runs SingleBinary with auth_enabled: false, and its HTTP port mixes
# reads with control endpoints that also answer GET (/ingester/shutdown,
# /flush). An RBAC `get` on services/proxy for loki:3100 would therefore be a
# kill switch for log ingest. This server is the read-only boundary instead:
# the paperclip read ServiceAccount may proxy to this Service only.
#
# Each allowed location forwards a URI built here, never the client's raw
# request URI, so dot-segments or encoded slashes cannot reach another Loki
# path. Anything not listed gets 403. /loki/api/v1/tail (websocket) is left
# out on purpose. Adding a path is a new card and a new review.
upstream loki {
server loki.monitoring.svc.cluster.local:3100;
}
server {
listen 8080;
server_tokens off;
proxy_http_version 1.1;
proxy_set_header Host loki;
proxy_read_timeout 60s;
location / {
return 403;
}
location = /healthz {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
location = /loki/api/v1/query {
limit_except GET { deny all; }
proxy_pass http://loki/loki/api/v1/query$is_args$args;
}
location = /loki/api/v1/query_range {
limit_except GET { deny all; }
proxy_pass http://loki/loki/api/v1/query_range$is_args$args;
}
location = /loki/api/v1/labels {
limit_except GET { deny all; }
proxy_pass http://loki/loki/api/v1/labels$is_args$args;
}
location = /loki/api/v1/series {
limit_except GET { deny all; }
proxy_pass http://loki/loki/api/v1/series$is_args$args;
}
location ~ ^/loki/api/v1/label/(?<label_name>[A-Za-z_][A-Za-z0-9_]*)/values$ {
limit_except GET { deny all; }
proxy_pass http://loki/loki/api/v1/label/$label_name/values$is_args$args;
}
}
@@ -0,0 +1,97 @@
---
# Read-only Loki gateway for the agents in the paperclip pod (REN-621,
# owner decision on REN-622). The allow-list and the reason it exists are in
# loki-read-gateway.conf. Callers reach it through the apiserver proxy only:
# /api/v1/namespaces/monitoring/services/loki-read-gateway:8080/proxy/loki/api/v1/query_range?...
# granted by ../paperclip/prometheus-read-rbac.yaml.
#
# Stateless and off the ingest path: Alloy pushes straight to the `loki`
# Service, so restarting or deleting this pod never touches log ingest.
# Revert: delete this file, the .conf and their kustomization entries, and the
# resourceName in the paperclip Role.
apiVersion: apps/v1
kind: Deployment
metadata:
name: loki-read-gateway
namespace: monitoring
labels:
app.kubernetes.io/name: loki-read-gateway
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: loki-read-gateway
template:
metadata:
labels:
app.kubernetes.io/name: loki-read-gateway
spec:
automountServiceAccountToken: false
enableServiceLinks: false
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
seccompProfile:
type: RuntimeDefault
containers:
- name: nginx
image: nginxinc/nginx-unprivileged:1.27-alpine
ports:
- name: http
containerPort: 8080
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
readinessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 30
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
memory: 64Mi
volumeMounts:
# Replaces the image's default.conf; the entrypoint's ipv6 and
# envsubst steps skip a read-only conf.d and the server starts.
- name: config
mountPath: /etc/nginx/conf.d
readOnly: true
# pid file and proxy temp paths of the unprivileged image.
- name: tmp
mountPath: /tmp
volumes:
- name: config
configMap:
name: loki-read-gateway
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 32Mi
---
apiVersion: v1
kind: Service
metadata:
name: loki-read-gateway
namespace: monitoring
labels:
app.kubernetes.io/name: loki-read-gateway
spec:
selector:
app.kubernetes.io/name: loki-read-gateway
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
@@ -24,10 +24,11 @@
#
# Every process in the paperclip pod can read the mounted ServiceAccount token,
# so the token is worth the sum of everything bound to this ServiceAccount. This
# Role's own share of that is `get` on the two Services named below and nothing
# Role's own share of that is `get` on the Services named below and nothing
# else. Widening it is a new card and a new review, not an edit here — the
# Alertmanager name was added that way (REN-95, for read-only alert-delivery
# checks; parent REN-94).
# checks; parent REN-94). The Loki read gateway was added that way too (REN-621,
# owner decision REN-622).
#
# THIS IS NO LONGER THE ONLY GRANT ON THIS ServiceAccount
#
@@ -43,6 +44,7 @@
# port-qualified forms are granted, so the only URLs that work are
# /api/v1/namespaces/monitoring/services/kube-prometheus-stack-prometheus:9090/proxy/...
# /api/v1/namespaces/monitoring/services/kube-prometheus-stack-alertmanager:9093/proxy/...
# /api/v1/namespaces/monitoring/services/loki-read-gateway:8080/proxy/loki/api/v1/...
# A 403 on some other form is the grant working. Do not fix it by adding a
# string.
apiVersion: v1
@@ -79,6 +81,11 @@ rules:
resourceNames:
- "kube-prometheus-stack-prometheus:9090"
- "kube-prometheus-stack-alertmanager:9093"
# REN-621 (owner decision REN-622): the GET-only Loki gateway, never
# `loki:3100` itself. Loki answers GET /ingester/shutdown and /flush,
# so a `get` on it would not be read-only. See
# ../monitoring/loki-read-gateway.conf.
- "loki-read-gateway:8080"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding