The receiver tier is an OTLP/HTTP endpoint for applications — Lambdas, services,
jobs — that sits behind your Gateway API gateway and forwards each tenant's data to
your tenant-aware backends (Mimir, Loki, Tempo) carrying that tenant's
X-Scope-OrgID. One receiver serves every tenant.
It is off by default (receiver.enabled: false): an opt-in
feature. Its security review (containment, a tested off-switch, the gateway as the only
ingress, a real-Alloy tenancy proof) is signed off, and it stays opt-in. It sets no CORS
headers, so a browser sending OTLP from another origin needs that handled in front of it.
client --> https://gw.example/otlp/<route-segment>/v1/traces
--> your gateway's HTTPRoute
strips the /otlp/<route-segment> prefix
SETS X-Scope-OrgID: <tenant> (replacing anything the client sent)
--> the receiver (Alloy, OTLP/HTTP)
forwards the request's X-Scope-OrgID with its data
--> your backend
The tenant comes from the route, not the client: a client that sends its own
X-Scope-OrgID has it overwritten by the gateway. The route segment in the URL is an
identifier, not a secret — URLs leak through logs and browser history.
Protect the endpoint with the gateway's own controls (origin allowlists, rate limits) and
rotate segments when they leak.
Because the tenant header is only trustworthy when the gateway set it, the receiver accepts
traffic only from your gateway's pods. Its NetworkPolicy admits the peers in
receiver.networkPolicy.gatewayFrom and nothing else, so a pod elsewhere cannot
reach it and claim a tenant.
receiver:
enabled: true
exporters:
traces:
enabled: true
endpoint: "http://tempo-distributor.observability:4318"
metrics:
enabled: true
endpointFromSecret: {name: mimir-otlp, key: endpoint}
authorizationFromSecret: {name: mimir-otlp, key: authorization}
networkPolicy:
# Your gateway's data-plane pods -- a podSelector is REQUIRED.
gatewayFrom:
- namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: gateways}}
podSelector: {matchLabels: {gateway.networking.k8s.io/gateway-name: edge}}
# Where the receiver may send data. Egress is otherwise denied (DNS aside).
egress:
- to:
- namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: observability}}
Gateway API implementations label their data-plane pods
gateway.networking.k8s.io/gateway-name: <gateway>, which is the selector
shown. Credentials go in a Secret and reach the receiver only as environment variables —
never in its ConfigMap.
gatewayFrom or egress — a receiver no gateway can
reach, or one that cannot deliver, fails helm template with a message.gatewayFrom peer without a non-empty podSelector — a
namespace alone would admit any pod in it, and any such pod could assert any tenant.
The receiver's config is rendered and validated when its pod starts. A config the renderer
refuses stops the new pod at Init:Error — the receiver already running keeps
serving — and kubectl logs <pod> -c render says why.
Create a tenant route on Delivery → Tenant routes (or through
TenantRouteService), naming the Gateway it attaches to. Shepherd then creates the
route's HTTPRoute in its own namespace, pointing at the receiver, and waits for the gateway to
report it attached. Only org admins create, rotate and revoke tenant routes, and an
organisation needs a tenant ID before it can have any. The In cluster
column shows the result:
allowedRoutes.namespaces to include it. Shepherd keeps checking, with a
backoff of up to 30 minutes, so the route turns applied after the
Gateway is fixed.receiver.applyTenantRoutes is false, or Shepherd is not running
in a cluster), or it is a Faro route, which has no receiver yet.
Rotating a route keeps the old path working until its overlap ends, then Shepherd revokes it
and deletes its HTTPRoute. Revoking deletes it straight away. Shepherd never creates or
changes a Gateway, and never touches HTTPRoutes it did not create (it finds its own by the
shepherd.io/tenant-route-id label).
Each active OTLP route has a Connect an app button (any org member can use
it). Enter the service name and Shepherd shows the endpoint to send to, plus ready-to-paste
configuration: a .env file, a Kubernetes container env block, Lambda
environment JSON, Terraform, SAM and CDK fragments, and notes for OpenTelemetry SDKs. Most SDKs
need no code change — the standard OTEL_EXPORTER_OTLP_* variables are
enough.
The endpoint uses your gateway's public address. Set it once with
receiver.publicBaseURL (it must be https://); otherwise users type it
in the dialog. Only OTLP over HTTP is offered: the gateway finds the tenant from the URL path,
which a gRPC client cannot set.
receiver:
publicBaseURL: https://telemetry.example.com
To do this, the chart gives Shepherd's ServiceAccount a Role on httproutes in the
release namespace and permission to read the one httproutes.gateway.networking.k8s.io
CRD (to check the Gateway API version), and mounts its token on the Shepherd pod. None of it
exists with the receiver off. To manage the HTTPRoutes yourself instead, set
receiver.applyTenantRoutes: false and write each one in this shape:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: acme-otlp
namespace: shepherd # the receiver's namespace
spec:
parentRefs:
- name: edge
namespace: gateways
rules:
- matches:
- path: {type: PathPrefix, value: /otlp/acme-7f3k2}
filters:
- type: URLRewrite
urlRewrite:
path: {type: ReplacePrefixMatch, replacePrefixMatch: ""}
- type: RequestHeaderModifier
requestHeaderModifier:
set: [{name: X-Scope-OrgID, value: acme}]
backendRefs:
- name: shepherd-receiver # <release>-receiver
port: 4318