Shepherd reads its settings from a YAML file and from SHEPHERD_* environment
variables, which override it. In a chart install the non-secret settings come from the
chart’s config: block, rendered into a ConfigMap mounted at
/etc/shepherd/shepherd.yaml, and the secrets below from a Kubernetes Secret; in
local development, from your shell (and ./shepherd.yaml if present). Only two
settings are required — the server refuses to start without them — and the rest
matter only for the setups they name.
In a chart install these are keys of the Kubernetes Secret the chart reads
(shepherd-secrets by default, or override existingSecret).
Running locally, they are plain environment variables.
| Key | Required? | Description |
|---|---|---|
SHEPHERD_DATABASE_URL | Yes | PostgreSQL connection string |
SHEPHERD_SECURITY_ENCRYPTION_KEY | Yes | Base64-encoded 32-byte key encrypting stored credentials at rest. Keep it — a replacement key cannot decrypt what the old one wrote |
SHEPHERD_BOOTSTRAP_ADMIN_PASSWORD | Recommended | Password for the administrator created on first boot. Read only while the users table is empty. Leave it unset and the account is created as admin/admin, which can do nothing until the password is changed |
SHEPHERD_BOOTSTRAP_ADMIN_LOGIN | No | Login of that first administrator; defaults to admin |
SHEPHERD_OIDC_CLIENT_SECRET | Chart-configured SSO only | Omit it to configure SSO from the UI instead |
SHEPHERD_GRAPH_CLIENT_SECRET | Entra + Graph lookup only | Not used by any other provider |
See deploy/helm/shepherd/values.yaml for the full set of chart options, including networkPolicy and autoscaling. The chart has no user-seeding options of its own — the bootstrap admin above is the only account a fresh install creates. (shepherd dev seed, which additionally creates local editor/viewer accounts, is a dev-only CLI command outside the chart; see internal/cli/dev.go.)
Where these values come from is a separate decision: see Secrets for supplying them yourself or having them generated, and Database for the connection string.
Keep SHEPHERD_SECURITY_ENCRYPTION_KEY. It encrypts git
credentials and the OIDC client secret at rest. A new key on the next boot cannot decrypt
what the old one wrote, and Shepherd has no way to tell you which stored secret it has just
lost — it simply fails to use them. Generate it once, store it where you store your
other secrets, and back it up with them.
SHEPHERD_BOOTSTRAP_ADMIN_LOGIN and SHEPHERD_BOOTSTRAP_ADMIN_PASSWORD
are read only while the users table is empty, on first boot. Set the password and the
account is yours. Leave it unset and the account is created as admin/admin
with a forced password change, which blocks every request except the change itself — so
an unattended install cannot quietly sit on a default credential. After first boot, accounts
are managed at Admin → Users; see Users and teams.
server.trusted_proxies
Shepherd throttles local sign-in per client address as well as per username. By default it
takes that address from the TCP connection and trusts no forwarding header, because any
client can send X-Forwarded-For, X-Real-IP or
True-Client-IP. Behind an ingress controller or gateway, though, every
connection comes from the proxy, so every browser shares one per-IP bucket:
about 60 failed sign-ins from anyone, signed in or not, block local sign-in for everyone
until they stop. OIDC sign-in is not throttled this way and is unaffected.
server.trusted_proxies (config.server.trusted_proxies in the chart;
SHEPHERD_SERVER_TRUSTED_PROXIES, comma-separated) lists the CIDRs your proxies
connect from. When a connection comes from one of them, and only then, Shepherd reads
X-Forwarded-For right to left, skips entries that are themselves trusted, and
uses the first one that is not. Entries a client adds itself sit to the left of what your
proxy writes, so they are never reached. X-Real-IP and
True-Client-IP are never read. An entry that does not parse as a plain
address (one carrying a port, for instance) stops the walk and the proxy’s own
address is used — the shared bucket, never a client-chosen one. Default: empty.
A bare address is taken as a single host. A /0, an IPv4 prefix shorter than
/8 or an IPv6 prefix shorter than /16 stops Shepherd at startup.
This only works if two things hold, and the next three sections are about them: the address
your proxy records is the client’s real one, and nothing else can connect from inside
the list.
ingress-nginx by default sets X-Forwarded-For to the
address it saw ($remote_addr), replacing whatever the client sent: one entry per
request, and the client cannot influence it. Leave
use-forwarded-headers, enable-real-ip and
use-proxy-protocol off unless you also narrow
proxy-real-ip-cidr to your real load balancer. Its default is
0.0.0.0/0, so with any of those on, ingress-nginx takes the client address from
whatever the client sent, and the “address it saw” becomes client-chosen
before it reaches Shepherd.
NGINX Gateway Fabric appends the address it saw
($proxy_add_x_forwarded_for). The same caution applies to its
NginxProxy rewriteClientIP setting: with loose
trustedAddresses, NGF accepts a client-supplied address as the one it saw.
If an HTTP load balancer in front of the proxy adds its own X-Forwarded-For
entry, its addresses belong in the list too.
config:
server:
# The IP pool the ingress-nginx controller pods get addresses from.
# ingress-nginx SETS X-Forwarded-For to the address it saw; keep
# use-forwarded-headers / enable-real-ip / use-proxy-protocol off
# unless proxy-real-ip-cidr names only your load balancer.
trusted_proxies:
- 10.244.0.0/16
config:
server:
# The IP pool the Gateway's NGINX data-plane pods get addresses from.
# NGF APPENDS the address it saw to X-Forwarded-For; keep NginxProxy
# rewriteClientIP off unless trustedAddresses names only your load balancer.
trusted_proxies:
- 10.244.0.0/16
When you trust the pod CIDR, the proxy’s Service needs
externalTrafficPolicy: Local (or PROXY protocol, with the proxy’s
own trust list narrowed to your load balancer as above). With the default
Cluster, kube-proxy rewrites the source of traffic that lands on a different
node to that node’s address. On overlay networks — Calico in IPIP or VXLAN mode,
flannel VXLAN, and the kind dev stack this repository ships — that address is the
node’s tunnel address, taken from the pod IP pool, so it falls
inside a pod-CIDR trusted_proxies and is skipped as a trusted hop. With
NGINX Gateway Fabric (which appends) the next entry to the left is whatever the client sent,
so the client chooses its own bucket again. With ingress-nginx (which replaces) everyone
arriving through that node shares one bucket.
Every address inside trusted_proxies is believed when it names a client. Trust
the whole pod CIDR and any pod in the cluster can claim any client address
— by connecting to Shepherd directly with a forged X-Forwarded-For, or by
going through the ingress, whose own address is trusted and skipped. Narrow it where you can:
networkPolicy.enabled policy admits every namespace today
(namespaceSelector: {}), so it does not do this; add your own policy selecting
the Shepherd server pods that allows port 8080 only from the proxy’s namespace and
from wherever your in-cluster collectors poll from, and the metrics port from your
Prometheus.
hostNetwork: true, its pods connect from
the node addresses, so the list needs the node CIDR instead of the pod CIDR, and
everything else on those nodes can then claim any address.
Get the list wrong the other way — a CIDR that does not contain the proxy — and Shepherd keeps using the proxy’s address: no spoofing, only the shared bucket.
One setting decides where SSO configuration comes from: oidc.issuer. Set it in
your chart values and the chart owns SSO; leave it unset and an app admin configures a
provider from the UI instead. Single sign-on covers both
paths and what each one costs you.