A destination is where an organisation’s telemetry is written: a
Prometheus remote-write endpoint (Mimir), a Loki push endpoint, or an OTLP endpoint. Org
admins manage them on the Destinations page. Wizards ship to them by name:
the destination a wizard step picks becomes that pipeline’s writer
(prometheus.remote_write or loki.write), including its
authentication.
Enter the full endpoint URL the collector writes to, for example
https://mimir.example.com/api/v1/push or
https://loki.example.com/loki/api/v1/push. Shepherd puts it into the writer
exactly as written and adds no path.
For a multi-tenant Mimir or Loki, set the destination’s Tenant ID.
Wizard pipelines that ship there send it as X-Scope-OrgID: as a header on
prometheus.remote_write, and as loki.write’s own
tenant_id. Leave it empty to send no tenant. It follows Mimir’s rule:
letters, digits and ! - _ . * ' ( ), at most 150 characters, no slashes or
spaces.
Any tenant your backend expects is allowed except one that belongs to another organisation in Shepherd (the tenant ID an application administrator gave it). That is refused when you save, and refused again when a wizard pipeline is generated, in case the other organisation was given that tenant ID later. The other way round, an administrator cannot give an organisation a tenant ID another organisation’s destination already sends.
This guards a destination’s tenant only. A pipeline you write by hand can
set any header, X-Scope-OrgID included; Shepherd does not inspect it.
A pipeline picks up a tenant change when it is regenerated: saving a destination regenerates every wizard pipeline that names it. Until then the configuration collectors are served keeps the tenant it was generated with. A pipeline that ships to more than one destination is regenerated as a whole, so saving any one of them also starts sending a tenant already stored on the others.
Shepherd never holds a telemetry backend’s credentials. For an authenticated
destination you name a Kubernetes Secret that already exists on every
cluster whose collectors ship there, and the collector reads it at runtime with
remote.kubernetes.secret. The served config contains the Secret’s
namespace, its name and the key names below, never the values.
| Auth mode | Keys the Secret must hold | What the writer gets |
|---|---|---|
| None | — | no auth block |
| Basic auth (Kubernetes Secret) | username, password |
basic_auth |
| OAuth2 (Kubernetes Secret) | client_id, client_secret, token_url |
oauth2 (client credentials) |
OAuth2 scopes are set on the destination itself (the OAuth2
scopes field, stored as extra.oauth2_scopes), not in the Secret. They are
not sensitive, and because Shepherd never reads the Secret it could not tell whether an
optional key is there.
Create the Secret in the namespace you name on the destination:
kubectl -n monitoring create secret generic mimir-credentials \
--from-literal=username=<user> \
--from-literal=password=<password>
A basic-auth destination named prom-prod pointing at that Secret gives a wizard
pipeline this writer:
remote.kubernetes.secret "metrics_auth" {
namespace = "monitoring"
name = "mimir-credentials"
}
prometheus.remote_write "metrics" {
endpoint {
name = "prom-prod"
url = "https://mimir.example.com/api/v1/push"
basic_auth {
username = convert.nonsensitive(remote.kubernetes.secret.metrics_auth.data["username"])
password = remote.kubernetes.secret.metrics_auth.data["password"]
}
}
}
For a backend behind a private CA, or one that requires a client certificate, open
TLS options on the destination. As with authentication, the certificates
stay on the spoke clusters: you name the objects, and the collector reads them at runtime
with remote.kubernetes.secret or remote.kubernetes.configmap. TLS
options need an https:// URL.
| Option | Object | Keys | What the writer gets |
|---|---|---|---|
| Trusted CA | a ConfigMap or a Secret | ca.crt, or the key you name | tls_config.ca_pem |
| Client certificate | a Secret | tls.crt, tls.key |
cert_pem, key_pem |
| Server name | — | — | server_name |
kubernetes.io/tls Secret, which
is what cert-manager writes. A cert-manager Secret also carries ca.crt, so it
can serve as both CA and client certificate; the collector then reads it once.Bundle target.kube-root-ca.crt ConfigMap in every namespace works as the CA when the
backend’s certificate was issued by the cluster CA.tls.crt or tls.key is caught
earlier — the collector refuses the whole config.The served config holds only the references and key names:
remote.kubernetes.configmap "metrics_tls_ca" {
namespace = "monitoring"
name = "backend-ca"
}
prometheus.remote_write "metrics" {
endpoint {
name = "prom-prod"
url = "https://mimir.example.com/api/v1/push"
tls_config {
ca_pem = remote.kubernetes.configmap.metrics_tls_ca.data["ca.crt"]
server_name = "mimir.internal.example"
}
}
}
The collector’s service account needs get on Secrets in the
Secret’s namespace: remote.kubernetes.secret reads the one named Secret
with a GET when the config loads and again every minute, and never lists or watches. The
Grafana Alloy Helm chart, and so k8s-monitoring’s collectors, grants Secret access
cluster-wide by default; check what your chart version grants for ConfigMaps. If your
collectors run with narrower RBAC, grant it in that one namespace. A destination whose CA
is in a ConfigMap needs get on configmaps in that namespace
(remote.kubernetes.configmap reads the same way) — the second rule
below. The first rule also grants list and watch, which are
not needed:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: alloy-read-destination-secrets
namespace: monitoring
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: alloy-read-destination-secrets
namespace: monitoring
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: alloy-read-destination-secrets
subjects:
- kind: ServiceAccount
name: <collector service account>
namespace: <collector namespace>
Shepherd’s Kubernetes test suite runs a real Alloy with this Role’s Secret rule and checks that its remote-write requests carry the Secret’s credentials.
The writer, auth block included, is generated when a wizard is previewed or committed. It is stored as part of the pipeline, so it passes the same validation as any other line and shows in the editor and its revision history. A wizard that names a destination the organisation does not have, or one of the wrong type, is refused with a message saying which.
Saving a change to a destination regenerates every wizard pipeline in the organisation that ships to it, from the answers stored with the pipeline, in the same step. Each regenerated pipeline passes the full validation gate, gets a new revision and an audit entry under your name, and collectors receive it on their next poll; the confirmation says how many were regenerated (Destination updated — 3 pipelines regenerated). Renaming a destination renames it in those pipelines too. If any of them would fail validation, nothing is saved and the message names the pipeline, the reason and what to do; each pipeline it names links to its page. The same happens when one of them was edited by hand after its wizard generated it: Shepherd never overwrites the edit. Such a pipeline is badged Not following wizard in the pipelines list and Hand-edited in its header, and the editor asks before the save that makes it one (Save a hand edit?). Open the pipeline and do one of three things, then save the destination again:
Running the wizard again does not help: it creates a new pipeline.
Shepherd recognises a hand edit by a fingerprint of the text the wizard last wrote, so a newer version of a wizard does not make older pipelines look edited. A wizard pipeline generated before that fingerprint existed is compared with what its wizard would generate today instead, until its next regeneration records one. If that wizard has changed since, such a pipeline reads Not following wizard although nobody edited it, and Restore last wizard version offers its current revision, which re-attaches it.
A destination a wizard pipeline still ships to cannot be deleted. The refusal lists the pipelines, each linked to its page; detach them from the wizard, or delete them, first.
Wizard pipelines committed before destination auth was rendered read their URL from
sys.env("SHEPHERD_DEST_<NAME>_URL"), which nothing sets, and carry no
auth. Convert them once with shepherd admin rerender-destinations, run with
the server's own configuration, --dry-run first:
kubectl -n shepherd exec svc/shepherd -- /usr/local/bin/shepherd admin rerender-destinations \
--config /etc/shepherd/shepherd.yaml --dry-run
It lists every pipeline it converts and every one it cannot (for example, one naming a destination that was deleted), and is safe to run again. Saving a destination also regenerates the pipelines that use it.
A destination’s tenant ID was stored but not sent before Shepherd rendered it. Such a
tenant reaches a pipeline when the pipeline is next regenerated, which moves that data into
the named backend tenant. To see which destinations carry a tenant and which pipelines would
change, run the same command with --all; it writes nothing until you add
--apply, and never overwrites a pipeline edited by hand:
kubectl -n shepherd exec svc/shepherd -- /usr/local/bin/shepherd admin rerender-destinations \
--config /etc/shepherd/shepherd.yaml --all