fix(crowdsec): drop the LAPI IPAllowList, it would have rejected the bouncer
The allowlist carried the published proxy addresses on the assumption that a request from a proxy host arrives carrying that host's public address. It does not. The client connects to the local proxy on 127.0.0.1, the reverse-proxy writes what it saw into X-Forwarded-For, and Traefik resolves that into ClientHost. Measured against netbird-proxy-watchdog, which makes exactly this kind of request every round: 172.18.0.3 - - [02/Sep/2026:10:12:01] "GET /" 200 websecure-immich-... 172.18.0.3 - - [02/Sep/2026:10:12:01] "GET /" 200 websecure-home-... Two probes one second apart, one per watchdog target. 172.18.0.3 is a Docker bridge address on the proxy side. So the allowlist would have 403ed the bouncer. Allowlisting 172.18.0.0/16 instead is not a control: it is a private range on a host whose addressing we do not own, and it would admit anything else that happens to originate there. The bouncer key stays as the access control, which is what it always was. The endpoint serves read-only decision streams to a valid key; the admin surface is cscli, in-cluster. If the source address ever becomes deterministic, an ipAllowList can go back on top. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,37 +10,27 @@
|
||||
# LAPI on a LoadBalancer VIP; this reuses the path every other service already
|
||||
# takes instead of adding two peers and an ACL policy.
|
||||
#
|
||||
# The exposure is narrowed twice over: the IPAllowList below, and the bouncer
|
||||
# API key. The LAPI serves read-only decision streams to a valid bouncer key;
|
||||
# it is not the admin surface (cscli talks to it in-cluster).
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: lapi-proxy-only
|
||||
namespace: crowdsec
|
||||
spec:
|
||||
ipAllowList:
|
||||
# The proxy hosts, from the A/AAAA records of proxy.renner.uno.
|
||||
#
|
||||
# THESE ARE THE PUBLISHED (INBOUND) ADDRESSES. A host's egress address is
|
||||
# not guaranteed to be the one it publishes, and proxy-2 has both families
|
||||
# to choose from. Confirm what actually arrives before relying on this —
|
||||
# a mismatch shows up as 403 in the Traefik access log, with the real
|
||||
# source in the X-Forwarded-For chain.
|
||||
sourceRange:
|
||||
- "45.157.177.26/32"
|
||||
- "2a03:4000:4b:c1e:4474:43ff:fedf:8f85/128"
|
||||
- "2a03:4000:28:f6e:a4c7:59ff:fe1e:9a29/128"
|
||||
ipStrategy:
|
||||
# Same CIDRs as the websecure entrypoint's forwardedHeaders and the
|
||||
# rate-limit middleware. Without this the allowlist would compare against
|
||||
# the netbird-ingress pod IP that re-originates every WAN-via-proxy
|
||||
# connection, which is the same shared-source-IP gap that caused the
|
||||
# 2026-06-27 CrowdSec IPv6 mass-ban. Excluding the pod CIDRs makes
|
||||
# Traefik walk back through X-Forwarded-For to the real client.
|
||||
excludedIPs:
|
||||
- "10.244.0.0/16"
|
||||
- "fd10:244::/56"
|
||||
# NO IP ALLOWLIST, deliberately. The bouncer key is the access control.
|
||||
#
|
||||
# An allowlist was the obvious second factor and it does not work here. A
|
||||
# request originating on a proxy host does not arrive carrying that host's
|
||||
# public address: the client connects to the local proxy on 127.0.0.1, the
|
||||
# reverse-proxy puts what it saw into X-Forwarded-For, and Traefik resolves
|
||||
# that. Measured against the netbird-proxy-watchdog, which makes exactly this
|
||||
# kind of request every round:
|
||||
#
|
||||
# 172.18.0.3 - - [02/Sep/2026:10:12:01] "GET /" 200 websecure-immich-...
|
||||
# 172.18.0.3 - - [02/Sep/2026:10:12:01] "GET /" 200 websecure-home-...
|
||||
#
|
||||
# 172.18.0.3 is a Docker bridge address on the proxy side. Allowlisting the
|
||||
# published proxy addresses would have rejected the bouncer with 403;
|
||||
# allowlisting 172.18.0.0/16 would be a private range on a host we do not
|
||||
# control the addressing of, which is not a security control.
|
||||
#
|
||||
# What remains is the API key, which is the real control anyway: this endpoint
|
||||
# serves read-only decision streams to a valid bouncer key and nothing else.
|
||||
# The admin surface is cscli, in-cluster. If the source address ever becomes
|
||||
# deterministic, an ipAllowList can be added on top.
|
||||
---
|
||||
# The LAPI serves HTTPS only once tls.enabled is set — there is no plaintext
|
||||
# fallback and no second port; 8080 is the TLS port. Its certificate comes
|
||||
@@ -70,7 +60,6 @@ metadata:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-cluster
|
||||
# NOT behind the Authentik gate: the client is a daemon with an API key,
|
||||
# not a browser that can complete an OIDC flow.
|
||||
traefik.ingress.kubernetes.io/router.middlewares: crowdsec-lapi-proxy-only@kubernetescrd
|
||||
traefik.ingress.kubernetes.io/service.serversscheme: https
|
||||
traefik.ingress.kubernetes.io/service.serverstransport: crowdsec-crowdsec-lapi@kubernetescrd
|
||||
spec:
|
||||
|
||||
Reference in New Issue
Block a user