Agent tokens

Collectors authenticate to Shepherd with an agent token over HTTP Basic auth: the token’s ID is the username, its secret the password.

  1. As an app admin, open Admin → Agent Tokens and create one. Copy the secret straight away — it is shown exactly once.
  2. Put both into the collector’s remotecfg block (keep the secret in an environment variable or a Kubernetes Secret, not in the file):
config.alloy
remotecfg {
  url = "https://shepherd.example.com"
  basic_auth {
    username = "<token-id>"
    password = sys.env("SHEPHERD_AGENT_TOKEN_SECRET")
  }
  attributes = { cluster = "prod-eu-1", role = "metrics" }
  poll_frequency = "60s"
}

A token is not tied to an organisation: which org a collector belongs to is decided by claiming its cluster (Admin → Clusters). Revoke a token on the same page; collectors using it are refused on their next poll. On Grafana’s k8s-monitoring chart, Connect a cluster writes this wiring for you.

To create tokens from a script instead, run the CLI inside the Shepherd pod, which already has the database settings:

shell
kubectl -n shepherd exec svc/shepherd -- /usr/local/bin/shepherd token create --name ci

Authenticate with OIDC instead of a shared token

A collector can present an OAuth2 access token from the same identity provider that does your single sign-on, rather than a shared agent token. Alloy’s remotecfg block fetches and presents it natively, so nothing changes on the agent beyond its config. With a binding (below) or IdP-asserted organisations, the token is scoped to one organisation, so a collector in one org can never pull another’s configuration; without either, the collector’s organisation comes from claiming its cluster, as with an agent token.

Turn it on in the chart under config.oidc (it stays off until agent_audience is set). These settings are read from chart config only, so collector OIDC needs config.oidc.issuer in your values, which also makes single sign-on chart-managed; with single sign-on set up in the admin UI, the agent_* settings are ignored.

values.yaml
config:
  oidc:
    issuer: "https://login.microsoftonline.com/<tenant>/v2.0"
    # The resource identifier a collector's token must carry in `aud`. This is
    # the master switch and the thing that separates a machine token from a
    # user login token.
    agent_audience: "api://shepherd-collectors"
    # Recommended: require the token to carry a specific app role, so a valid
    # token for the audience is not enough without the explicit grant.
    agent_required_role: "Collector.Poll"
    # Or, if your provider grants access as a scope: agent_required_scope.
    # The claim naming the calling client, matched against a binding's app id.
    # Defaults to sub; for Entra client-credentials tokens use azp.
    agent_app_claim: "azp"

Then bind each collector’s client identity to an organisation. By default Shepherd holds the mapping — the identity provider is not authoritative for which org a collector belongs to (see Trusting the IdP for org assignment to opt into the alternative):

shell
shepherd agent-identity create \
  --issuer "https://login.microsoftonline.com/<tenant>/v2.0" \
  --app-id "<client-id>" \
  --org "platform-engineering" \
  --cluster "prod-eu-1"        # optional; repeatable. Omit for any cluster in the org.

A collector whose cluster is unclaimed is auto-assigned to its bound org on its first authenticated poll. A cluster already claimed by a different org is refused. A binding can also restrict the collector roles it admits (--role, repeatable). App admins can manage bindings in the UI too, under Admin → Single sign-on → Collector identity bindings.

Trusting the IdP for org assignment (optional)

Instead of a Shepherd-side binding, you can let the identity provider assert the org, if you trust it to. Off by default; turn it on with one of two config values:

  • oidc.agent_trust_org_claim — the name of a token claim whose value is the org’s name (a custom claim your IdP emits, e.g. shepherd_org).
  • oidc.agent_org_role_prefix — read the org from an app role under this prefix, the “app role names the org” pattern. With prefix shepherd-org:, the role shepherd-org:platform-engineering assigns the collector to platform-engineering.

The asserted value is an org name, resolved against your organisations and then auto-claiming the cluster exactly as a binding would. An org that does not exist is refused; a role prefix that matches two different orgs is treated as no assertion. A token that carries neither a binding nor a trusted org claim still falls through to the admin cluster-claim. A binding, when present, wins over an asserted claim.

The Alloy side

Point remotecfg at Shepherd with an oauth2 block instead of basic_auth. Alloy fetches its own access token from the provider and presents it on every poll:

config.alloy
remotecfg {
  url = "https://shepherd.example.com"
  oauth2 {
    client_id     = sys.env("SHEPHERD_OIDC_CLIENT_ID")
    client_secret = sys.env("SHEPHERD_OIDC_CLIENT_SECRET")
    token_url     = "https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token"
    scopes        = ["api://shepherd-collectors/.default"]
  }
  attributes     = { cluster = "prod-eu-1", role = "metrics" }
  poll_frequency = "60s"
}

The IdP must issue JWT access tokens with the configured audience. This is the standard OAuth2 client-credentials grant: in Entra ID, expose an API on Shepherd’s app registration, define an app role, and grant it to each collector’s own app registration. Keycloak, Auth0 and Okta express the same thing with a client role, a permission, or a scope — set agent_roles_claim to whichever claim your provider emits.

Self-monitoring over OIDC

To send the collector’s own health beacon over the same credential, set config.oidc.beacon_auth: oauth2 and agent_token_url. Shepherd then renders the beacon’s write-back with an oauth2 block too. This is a whole-fleet choice: the served configuration is shared across a collector’s instances, so a deployment uses one beacon-auth mode.