Every value the chart accepts, generated from
values.yaml
in chart 0.22.0. The descriptions are the chart’s own comments, so this
page cannot describe a value the chart does not have, or miss one it gained.
This page is generated from the chart on main, which can be slightly
ahead of the newest published chart. If a value here is missing from the version you
installed, check
the releases — or run
helm show values oci://ghcr.io/procoduck/charts/shepherd --version <yours>,
which answers for the exact chart you have.
Only two values are ever required, and neither is here: the database URL
and the encryption key live in a Secret, not in values. See
Configuration.
nameOverride
Key
Default
Description
nameOverride
""
nameOverride: replaces the chart name in every generated object name and in the app.kubernetes.io/name label. Empty means "shepherd".
fullnameOverride
Key
Default
Description
fullnameOverride
""
fullnameOverride: replaces the whole "<release>-<chart>" prefix, so objects are named exactly this. Empty means the usual release-derived name.
image
Key
Default
Description
image.registry
"ghcr.io/procoduck"
Default to the released images; set registry: "" to use a bare/local name.
image.repository
shepherd
repository: the image name, combined with registry above (or used bare if registry is empty) to form the image the chart pulls.
image.tag
""
Empty means the chart's own appVersion, so a given chart version installs the Shepherd release it was published and tested with. Override only to pin something else deliberately.
image.pullPolicy
IfNotPresent
pullPolicy: standard Kubernetes image pull policy. IfNotPresent is safe here because tag defaults to a specific released version, not a moving tag like "latest".
image.pullSecrets
[]
pullSecrets: names of existing imagePullSecrets, for a private registry. Empty means the pod pulls with no credentials.
replicas
Key
Default
Description
replicas
2
Shepherd keeps no state outside PostgreSQL, so replicas are free to add and safe to lose. Two by default so a node drain is not an outage; the PodDisruptionBudget below assumes at least two.
service
The Service in front of Shepherd. ClusterIP by default: an Ingress or HTTPRoute (both below) is the intended way in for a real deployment. type is exposed because the template hardcoded ClusterIP, which left no way to reach Shepherd on a bare cluster without editing the rendered object by hand — kind and other local clusters have no ingress controller, and that is exactly where someone is trying the chart for the first time.
Key
Default
Description
service.type
ClusterIP
type: the Service's kind (ClusterIP, NodePort, LoadBalancer -- standard Kubernetes values). See above for why this is exposed rather than hardcoded to ClusterIP.
service.port
8080
port: the container's HTTP port and the port the Service listens on. config.server.listen below must agree with it -- the Deployment's containerPort is hardcoded to 8080, not derived from either value.
service.nodePort
""
Only used when type is NodePort. Leave empty to let Kubernetes pick one from the node port range.
service.appProtocol
kubernetes.io/h2c
appProtocol: the application-protocol hint on the Service port. Shepherd serves Connect over both HTTP/1.1 and cleartext HTTP/2 (h2c); the default tells gateways that honour the hint (Envoy-based ones, Istio) to speak h2c to it. NGINX Gateway Fabric refuses to proxy an HTTP route to an h2c upstream ("UnsupportedProtocol"), so with route.enabled on NGF set this to "" -- the field is then omitted and the gateway uses HTTP/1.1, which Shepherd serves equally well.
config
Key
Default
Description
config.server.listen
":8080"
listen: host:port the main HTTP server binds. The Deployment's containerPort and service.port above are both hardcoded to 8080, so changing this alone does not move where the container is reachable -- keep all three in agreement.
config.server.metrics_listen
":9090"
Metrics listen on their own port; /metrics is never served on listen. The container port, the metrics Service, and the ServiceMonitor are all derived from this value, so changing it here is enough.
config.server.base_url
"https://shepherd.example.internal"
base_url: Shepherd's own externally-reachable URL, no path. Collectors' remotecfg clients and the beacon's remote-write endpoint are both built against it, so it needs to be an address they can actually reach, not just this Service's in-cluster name.
config.server.beacon_disabled
false
The beacon (docs/gateway-tier-plan.md D6) serves every claimed collector a small baseline pipeline that reports which components it is running and whether they are healthy — component names only, never config text and never raw samples — and mounts an ingest endpoint at /beacon/v1/write authenticated by the same agent token everything else uses. It is ON by default and that is deliberate: the collector nobody knows about is exactly the one that would never opt in. Set this to true to turn it off entirely — no endpoint is mounted and no baseline is served. Opting out is offered because the beacon stores inventory and accepts writes, and an operator who cannot answer for that yet should be able to say no without downgrading.
config.server.trusted_proxies
[]
trusted_proxies: CIDRs of the reverse proxies directly in front of Shepherd -- your ingress controller or Gateway data-plane pods, usually the cluster's pod CIDR. Only when a connection comes from inside one of these is the client address read from X-Forwarded-For (right to left, skipping trusted hops); X-Real-IP and True-Client-IP are never read. Empty (the default) trusts no forwarding header: every browser behind the ingress then shares ONE per-IP local sign-in throttle bucket, which any unauthenticated client can exhaust (~60 failed attempts) to block local sign-in for everyone; OIDC sign-in is unaffected. Set this if people sign in with local accounts. Every address inside the list can claim ANY client address, so: give the proxy's Service externalTrafficPolicy: Local (with Cluster, on overlay CNIs the node's tunnel address -- from the pod pool -- is trusted too); keep ingress-nginx's use-forwarded-headers / enable-real-ip / use-proxy-protocol (and NGF's rewriteClientIP) off unless their trust list names only your load balancer; and narrow the list, or restrict who can reach Shepherd with your own NetworkPolicy (networkPolicy below admits every namespace). /0, IPv4 shorter than /8 and IPv6 shorter than /16 are rejected. Details and examples: the Configuration docs page, "Client addresses behind an ingress".
config.database.max_conns
20
max_conns: the maximum size of Shepherd's own PostgreSQL connection pool.
config.oidc.scopes
["openid", "profile", "email"]
scopes: OAuth scopes requested at login. This default matches every supported provider's minimum; add to it only if your provider requires more to return the claims configured below.
config.auth.session_ttl
"8h"
session_ttl: how long a signed-in session stays valid before the user must sign in again.
config.graph.base_url
"https://graph.microsoft.com"
base_url: the Microsoft Graph API root used to resolve group membership when use_graph_groups is on. Override only for a sovereign cloud (e.g. graph.microsoft.us) whose API root differs from the public cloud.
config.agent.inactive_after
"5m"
inactive_after: show a collector instance as inactive once this long has passed with no check-in (derived from its last check-in when read; its last reported status is kept and shows again when it comes back). Empty or 0: never shown inactive.
config.agent.delete_after
"24h"
delete_after: hard-delete a collector instance once this long has passed with no check-in. Longer than inactive_after, so an instance spends time visibly inactive before it disappears.
config.validate.alloy_binary
"/usr/local/bin/alloy"
alloy_binary: path to the bundled alloy binary Stage 2 validation execs. Empty skips Stage 2 entirely (reported as "skipped", not "valid", so a broken path is visible rather than silently passing).
config.validate.stability_level
"experimental"
stability_level: passed to alloy validate --stability.level=. One of stable, public-preview, experimental; a typo here fails every pipeline validation and nothing catches it at install time.
config.validate.timeout
"10s"
timeout: how long Stage 2's alloy validate exec is allowed to run before it is killed.
config.gitsync.tick
"3m"
tick: how often the git-sync reconciler polls repos that are due for a check, per their own poll interval.
config.tracing.enabled
false
Set enabled: true AND an endpoint. Enabled with no endpoint is reported as a misconfiguration in the logs rather than silently doing nothing — an empty trace backend with no explanation is the worst way to find out.
config.tracing.endpoint
""
endpoint: the OTLP collector's address, e.g. "opentelemetry-collector.observability.svc:4317" -- host:port, no scheme. Required when enabled is true; see enabled above.
config.tracing.protocol
"grpc"
protocol: "grpc" (collector port 4317, the default) or "http" (port 4318) -- which OTLP transport to export spans over.
config.tracing.insecure
true
insecure: skip TLS to the collector. true is normal for an in-cluster collector reached over the pod network; set false for a collector reached over the open network.
config.tracing.sample_ratio
0.1
sample_ratio applies only to traces Shepherd ROOTS; an incoming sampling decision is always honoured. Collectors poll continuously, so 1.0 here is a lot of spans.
config.tracing.service_name
"shepherd"
service_name: the resource.service.name attribute stamped on every exported span, so your trace backend can group Shepherd's spans apart from other services.
config.log.level
"info"
level: the minimum log level Shepherd emits, e.g. debug, info, warn, error.
config.log.format
"json"
format: the log encoding Shepherd writes -- "json" for structured logs (the default, suited to log aggregation) or "text" for human-readable output.
cnpg
----------------------------------------------------------------------------- CloudNativePG: let the chart provision the database. OFF by default, and Shepherd is happy either way — point SHEPHERD_DATABASE_URL at an RDS, a Cloud SQL, or any Postgres you already run and leave this alone. Turn it on and the chart renders a postgresql.cnpg.io/v1 Cluster, and wires Shepherd's database URL to the uri key of the <cluster>-app Secret that CNPG generates. You never write a connection string. Requires the CloudNativePG operator and its CRDs to be installed in the cluster already; the chart does not install operators. The Cluster renders as a pre-install/pre-upgrade hook so it exists before the migration Job runs. Three consequences worth knowing: - helm uninstall does NOT delete it. Hook resources are not tracked in the release, which for the thing holding all your data is the right way round. Remove it deliberately with kubectl delete cluster <name>. - Once it exists, changes to cnpg.* no longer reach it: the chart skips rendering a Cluster that is already there, because a hook Helm re-renders is a hook Helm first DELETES. Edit a live cluster with kubectl edit cluster <name>, which is the operator's own interface for it. - On a first install the migration Job may fail a few times while Postgres is still coming up. That is expected and it retries — see migrations.job.backoffLimit. GITOPS WARNING. The "skips rendering a Cluster that is already there" trick above uses lookup, which needs a live cluster connection. Argo CD, Flux and helm template | kubectl apply render the chart with no connection, so the guard fails OPEN there: the Cluster is emitted on every sync, and Argo CD in particular treats a pre-install/pre-upgrade hook as a PreSync hook it DELETES before recreating -- which takes the PVCs and the database with it. If you deploy this chart through any such tool, set render: never below once the database exists.
Key
Default
Description
cnpg.enabled
false
enabled: turn on to let the chart provision the database via CloudNativePG. See the block comment above for what that means and its GitOps caveat.
cnpg.render
auto
render: auto | always | never -- when to emit the Cluster object. auto (default) only when lookup says it is not already there. Correct under the helm CLI; unsafe under GitOps, see above. never never emit it. What a GitOps deployment sets after bootstrap. always emit it unconditionally, for bootstrapping or for a pipeline that wants the full desired state.
cnpg.name
""
Cluster name. Empty means "<release>-db".
cnpg.instances
2
Two gives you a primary and one standby, which is the minimum CloudNativePG needs for automated failover. Raise it for more read capacity or headroom.
cnpg.storage.size
10Gi
size: the PersistentVolumeClaim size for the primary and each standby.
cnpg.storage.storageClass
""
Empty uses the cluster's default StorageClass.
cnpg.database
shepherd
The database and the role that owns it. Shepherd needs no superuser.
cnpg.owner
shepherd
owner: the role that owns database above; Shepherd connects using this role. See the comment above -- no superuser required.
cnpg.imageName
""
Pinning the image is optional; empty lets the operator choose the version it was built against, which is the one it knows how to upgrade.
cnpg.resources
{}
resources: CPU/memory requests and limits for the Postgres instances. Empty means unbounded, same as the app's own resources below.
cnpg.extraSpec
{}
Merged verbatim into the Cluster spec, for anything this chart does not surface: backup, monitoring, affinity, postgresql.parameters, and so on. See the CloudNativePG API reference.
externalSecrets
----------------------------------------------------------------------------- External Secrets Operator: let the cluster generate the bootstrap secrets. OFF by default. Enabled, the chart renders two Password generators and an ExternalSecret that produces the Secret Shepherd reads — so the encryption key and the first administrator's password are generated in-cluster and never pass through your shell, your values file, or your terminal history. Requires the External Secrets Operator and its CRDs; the chart does not install operators. No SecretStore is needed: a generator is the source. refreshInterval is "0" and MUST stay "0". These are not rotatable credentials: regenerating the encryption key would leave every git credential and OIDC client secret already stored in the database undecryptable, with no error at the moment it happens. deletionPolicy is Retain for the same reason — uninstalling the release must not take the key with it. Both only hold while the ExternalSecret itself survives, so the chart also skips rendering it once it exists. Changing externalSecrets.* after the first install therefore has no effect; delete the ExternalSecret deliberately if you genuinely want it regenerated, understanding that a new key orphans every credential the old one encrypted. The same GITOPS WARNING as cnpg above applies, and here the loss is quieter: under Argo CD or Flux the ExternalSecret is recreated on every sync, syncs once against fresh Password generators, and replaces an encryption key that cannot be rotated -- with no error at the moment it happens. Set render: never below once the secret exists.
Key
Default
Description
externalSecrets.enabled
false
enabled: turn on to let External Secrets Operator generate the bootstrap Secret in-cluster. See the block comment above for what that means and its GitOps caveat.
externalSecrets.render
auto
render: auto | always | never. Identical meaning to cnpg.render above.
externalSecrets.apiVersion
external-secrets.io/v1
API versions, exposed because ESO has moved them before. These defaults match ESO v1.x.
externalSecrets.generatorApiVersion
generators.external-secrets.io/v1alpha1
generatorApiVersion: API version for the Password generator objects (distinct from apiVersion above, which is for the ExternalSecret itself).
externalSecrets.refreshInterval
"0"
Do not change. See the note above.
externalSecrets.encryptionKey.length
32
32 characters, base64-encoded by the ExternalSecret template into the 32 bytes Shepherd requires. DO NOT CHANGE: Shepherd rejects any other length outright, so a different value here produces an instance that cannot start. Exposed only because the generator requires the field.
externalSecrets.bootstrapAdmin.enabled
true
Generate the first administrator's password too. Turn this off if you would rather set SHEPHERD_BOOTSTRAP_ADMIN_PASSWORD yourself.
externalSecrets.bootstrapAdmin.length
24
length: character length of the generated bootstrap administrator password.
externalSecrets.bootstrapAdmin.login
admin
Read it once it exists with: kubectl get secret <release>-secrets \ -o jsonpath='{.data.SHEPHERD_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d
existingSecret
Key
Default
Description
existingSecret
""
existingSecret: the name of a Secret you already manage, holding the SHEPHERD_* environment variables. Empty means the chart renders its own from secrets below. Set this and secrets is ignored entirely; it is also mutually exclusive with externalSecrets.enabled, which the chart refuses rather than silently picking a winner.
secrets
Key
Default
Description
secrets
{}
secrets: key-value pairs rendered into a Kubernetes Secret and injected as environment variables. Convenient, and the least private of the three options: whatever you put here is stored verbatim in the Helm release and stays readable through helm get values <release> for the life of the release history. For production prefer existingSecret (you manage the object) or externalSecrets.enabled (the cluster generates it and it never passes through your shell at all). Keys must match the SHEPHERD_* env var names. Example for local admin password hash: secrets: SHEPHERD_DATABASE_URL: "postgres://..." SHEPHERD_SECURITY_ENCRYPTION_KEY: "base64-32-bytes" SHEPHERD_BOOTSTRAP_ADMIN_PASSWORD: "choose-something" NEVER put SHEPHERD_BOOTSTRAP_ADMIN_PASSWORD in the config: block above — that renders into a ConfigMap, which is not a place for a password.
migrations
Key
Default
Description
migrations.job.enabled
true
enabled: run shepherd migrate up as a pre-install/pre-upgrade hook, so the schema is always current before the new pods start. Turn it off only if you run migrations yourself (shepherd migrate up against the same database) BEFORE every upgrade -- Shepherd will not start against a schema older than its binary.
migrations.job.backoffLimit
6
How many times the migration Job may fail before the release does. The default covers a database that is still being provisioned -- see the Job template. Lower it if you would rather fail fast against a database that is always already there.
migrations.job.resources
{}
resources for the migration pod. Empty means unbounded, which is fine on most clusters; set it if a ResourceQuota or a LimitRange in this namespace requires every pod to declare one, since the Job is a pre-install hook and a rejected hook pod fails the whole install.
route
Gateway API HTTPRoute. OFF by default, like ingress below: it needs the Gateway API CRDs, which are not present on a vanilla cluster, so defaulting it on made a plain helm install fail with "no matches for kind HTTPRoute". Templating could never catch that — helm template does not check whether a Kind exists — and every values file in the repo already set it to false, which is how the broken default survived. Enable it deliberately, and set parentRefs/hostnames when you do: an HTTPRoute with neither routes nothing.
Key
Default
Description
route.enabled
false
enabled: turn on to render the HTTPRoute below. See the comment above for why it defaults off.
route.hostnames
[]
hostnames: hostnames this HTTPRoute matches, e.g. ["shepherd.example.com"]. Required alongside parentRefs -- an HTTPRoute with neither routes nothing.
route.parentRefs
[]
parentRefs: the Gateway(s) this HTTPRoute attaches to, e.g. [{name: my-gateway, namespace: gateway-system}]. Required alongside hostnames.
ingress
Key
Default
Description
ingress.enabled
false
enabled: turn on to render the Ingress below. Off by default: like route above, it needs infrastructure (an Ingress controller) a vanilla cluster does not have.
ingress.className
""
className: the IngressClass to use, e.g. "nginx". Empty lets the cluster's default IngressClass apply.
ingress.hosts
[]
hosts: a list of {host, paths: [{path, pathType}]} entries, the standard Kubernetes Ingress rule shape. Empty renders an Ingress with no rules, which routes nothing.
ingress.tls
[]
tls: standard Kubernetes Ingress TLS entries ({hosts, secretName}), for terminating TLS at the Ingress. Empty configures no TLS here.
metrics
--------------------------------------------------------------------------- Metrics Shepherd serves Prometheus metrics on a SEPARATE listener from the API (config.server.metrics_listen, default :9090). /metrics on the main port is deliberately 404-guarded, so anything scraping :8080/metrics gets nothing — which is what the ServiceMonitor here used to do. enabled controls the metrics Service and its container port. It is on by default: the listener runs regardless, and a ClusterIP Service in front of it exposes nothing outside the cluster. Turn it off only if you have a policy against unscraped Services existing. ---------------------------------------------------------------------------
Key
Default
Description
metrics.enabled
true
enabled: render the metrics Service and expose config.server.metrics_listen's port on it. The listener itself always runs regardless; this only controls whether a Service fronts it.
metrics.serviceMonitor.enabled
false
Requires the Prometheus Operator's CRDs. Off by default because a ServiceMonitor on a cluster without them fails the install.
metrics.serviceMonitor.labels
{}
labels the Prometheus Operator selects ServiceMonitors by — commonly something like release: kube-prometheus-stack. Check your Prometheus resource's serviceMonitorSelector; without a matching label the ServiceMonitor is created and quietly ignored.
metrics.serviceMonitor.interval
"30s"
interval: Prometheus scrape interval for this target. Empty uses the Prometheus Operator's own default.
metrics.serviceMonitor.scrapeTimeout
"10s"
scrapeTimeout: how long Prometheus waits for a scrape before giving up. Empty uses the Prometheus Operator's own default.
metrics.serviceMonitor.relabelings
[]
relabelings: standard Prometheus Operator relabeling rules applied before scraping (e.g. to rewrite target labels). Empty applies none.
metrics.serviceMonitor.metricRelabelings
[]
metricRelabelings: standard Prometheus Operator relabeling rules applied to each scraped metric after collection (e.g. to drop or rename series). Empty applies none.
serviceAccount
serviceAccount: the identity Shepherd's pods run as. By default the chart grants it no RBAC and Shepherd never calls the Kubernetes API. The one exception is the receiver tier: with receiver.enabled and receiver.applyTenantRoutes (its default) both on, the chart binds this account to a Role that writes HTTPRoutes in the release namespace and a ClusterRole that reads the single HTTPRoute CustomResourceDefinition, so Shepherd can apply tenant routes (templates/rbac-tenant-routes.yaml). Either way the account also exists to be annotated. That is how a pod reaches a managed database without a static credential: an EKS IRSA role ARN or a GKE Workload Identity binding goes in annotations below, and the token it produces is what authenticates to RDS or Cloud SQL.
Key
Default
Description
serviceAccount.create
true
create: false to use an account your platform team already manages, in which case name is required and must already exist — the migration Job runs as a pre-install hook and a pod naming a missing ServiceAccount is refused.
serviceAccount.name
""
name: empty means the release's fullname.
serviceAccount.annotations
{}
annotations: e.g. eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/shepherd iam.gke.io/gcp-service-account: shepherd@project.iam.gserviceaccount.com
resources
Key
Default
Description
resources
{}
resources: CPU/memory requests and limits for the Shepherd container. Empty means unbounded, which is deliberate for a first install — but set it before you rely on the deployment: requests are what the scheduler places on, and autoscaling below REQUIRES resources.requests.cpu (a CPU-utilization HPA is a percentage of the request, so with no request it can never scale). The chart refuses to render an HPA without one rather than leaving you a silently broken autoscaler.
nodeSelector
Key
Default
Description
nodeSelector
{}
nodeSelector: constrain Shepherd's pods to nodes carrying these labels.
tolerations
Key
Default
Description
tolerations
[]
tolerations: let Shepherd's pods schedule onto tainted nodes.
affinity
Key
Default
Description
affinity
{}
affinity: pod/node affinity rules, verbatim. Use this to spread replicas across zones — nothing here does that for you.
priorityClassName
Key
Default
Description
priorityClassName
""
priorityClassName: the PriorityClass Shepherd's pods run at, deciding what gets evicted first under node pressure.
podAnnotations
Key
Default
Description
podAnnotations
{}
podAnnotations: extra annotations on the Shepherd pod template. Merged with the chart's own checksum/config and checksum/secret annotations.
extraEnv
Key
Default
Description
extraEnv
[]
extraEnv: additional container env vars, verbatim (a list of Kubernetes EnvVar). This is where SHEPHERD_DATABASE_URL goes when you point Shepherd at a database you already run and are not using secrets/existingSecret.
autoscaling
autoscaling: hand the replica count to a HorizontalPodAutoscaler. When enabled, replicas above is no longer rendered at all — otherwise every helm upgrade would snap the Deployment back to it, undoing whatever the autoscaler had decided. The PodDisruptionBudget follows minReplicas rather than replicas for the same reason. REQUIRES resources.requests.cpu; see the note there.
Key
Default
Description
autoscaling.enabled
false
enabled: hand the replica count to a HorizontalPodAutoscaler. See the block comment above -- REQUIRES resources.requests.cpu.
autoscaling.minReplicas
2
minReplicas: the floor the HPA will not scale below.
autoscaling.maxReplicas
10
maxReplicas: the ceiling the HPA will not scale above.
autoscaling.targetCPUUtilizationPercentage
80
targetCPUUtilizationPercentage: the average CPU utilization, as a percentage of resources.requests.cpu, the HPA scales to maintain.
networkPolicy
networkPolicy: a NetworkPolicy for SHEPHERD's own pods (the simulator has its own, far stricter one under simulator.networkPolicy). Enabled, it allows ingress from any namespace in the cluster to the API port and the metrics port, and allows all egress — Shepherd has to reach your database, your identity provider and your git remotes, and the set of those is not something the chart can know. Restrict egress at the CNI level if you need FQDN policies. Off by default because a policy is only meaningful on a CNI that enforces them.
Key
Default
Description
networkPolicy.enabled
false
enabled: render a NetworkPolicy for Shepherd's own pods. See the block comment above for what it allows and why it defaults off.
simulator
----------------------------------------------------------------------------- S3 sandbox simulator (VB-1 §6.4) — ENABLED BY DEFAULT since v0.0.1, after both containment gates closed (docs/project-status.md F5: B-CONTAIN-1 fixed and red/green-proven; NetworkPolicy enforcement verified in a real cluster by e2e/k8s/simulator_containment_test.go's probes and kill probe). TO DISABLE: set simulator.enabled: false below (or --set on install) — nothing simulator-related renders, and shepherd's config is not wired to it. The compose stacks keep their opt-in sim profile: that is test-environment design (the default make e2e deliberately exercises the simulator-absent path), not the product default. When enabled, shepherd's config is wired to this chart's simulator Service automatically unless you set config.simulator yourself — an explicit config.simulator block always wins verbatim (see templates/configmap.yaml). It runs USER-AUTHORED Alloy configuration. Every control below is load-bearing, not decoration, and every one is asserted by deploy/helm/chart_test.go so it cannot regress silently: - automountServiceAccountToken: false on its ServiceAccount. Defense in depth, not the only gate — internal/simsvc.Config.SATokenPath makes the binary itself refuse to start if a token is mounted anyway. - a bearer token (simulator.token below) on the control API. Without one POST /v1/runs was reachable by anything that could route to the Pod at all; every install now authenticates it, from one of three sources. - a default-deny egress NetworkPolicy. The sandboxed Alloy is a CHILD PROCESS of shepherd-simulator (internal/simsvc/runner.go), not a sibling container, so this Pod's own network boundary IS the sandbox's boundary. Egress is allowed only to this Pod's own harness ports (the capture receivers shepherd-simulator itself serves) — nothing else, including cluster DNS (D10: every harness endpoint the sandbox needs is dialled by loopback IP, so it has no legitimate reason to resolve a name at all), the rest of the cluster, or the internet. - non-root securityContext, every capability dropped, no privilege escalation, a read-only root filesystem (the run scratch directory is an emptyDir mounted over /tmp). - CPU/memory limits, so one runaway sandboxed config cannot starve the node. - what this chart CANNOT set: a per-Pod process/PID limit. Kubernetes has no PodSpec field for one (unlike the compose stacks' pids_limit) — it is a kubelet-level setting (podPidsLimit in KubeletConfiguration, or --pod-max-pids) that belongs to the cluster operator, not this chart. Mitigating factors: the sandboxed Alloy is one process (internal/simsvc runs it directly, no shell fork-bomb surface), and the CPU/memory limits above already bound a runaway config's other resource costs. See docs/visual-builder-design-VB1.md §6.4 and docs/archive/reviews/s3-sandbox-security-findings.md (finding H5) for why each of these exists.
Key
Default
Description
simulator.enabled
true
enabled: turn the S3 sandbox simulator on or off. See the block comment above -- ON by default since v0.0.1, after both containment gates closed.
simulator.image.registry
"ghcr.io/procoduck"
registry: the simulator image's registry, mirroring image.registry above. Set to "" to use a bare/local name.
simulator.image.repository
shepherd-simulator
repository: the simulator image name, combined with registry above to form the pulled image.
simulator.image.tag
""
tag: empty means the chart's own appVersion, same convention as image.tag above.
simulator.image.pullPolicy
IfNotPresent
pullPolicy: standard Kubernetes pull policy, mirroring image.pullPolicy above.
simulator.image.pullSecrets
[]
pullSecrets for the simulator image specifically. Separate from image.pullSecrets because the two images can come from different registries; set both if they come from the same private one.
simulator.replicas
1
replicas: how many simulator Deployment pods run behind the simulator Service and its control API.
simulator.resources.requests.cpu
250m
cpu: minimum CPU reserved for the sandboxed Alloy process and its harness.
simulator.resources.requests.memory
256Mi
memory: minimum memory reserved, same reasoning as cpu above.
simulator.resources.limits.cpu
"1"
cpu: the hard CPU ceiling -- part of §6.4's defense against a runaway sandboxed config; see the block comment above.
simulator.resources.limits.memory
512Mi
memory: the hard memory ceiling, same reasoning as cpu above.
simulator.allowedHosts
[]
allowedHosts overrides SIM_ALLOWED_HOSTS, the endpoint allowlist that is §6.4's defense-in-depth check ON TOP OF the NetworkPolicy. Empty uses the service's own built-in default (its own hostname plus localhost).
simulator.token.value
""
value: pin the shared bearer token to a literal, e.g. for a fixture-driven parity check. Empty lets the chart generate and reuse a random one instead -- see the block comment above for the full precedence.
simulator.token.existingSecret
""
existingSecret: name of a Secret you already manage holding the token. Highest-precedence option -- see the block comment above.
simulator.token.key
"token"
key: the key within existingSecret (or the chart-generated Secret) that holds the token value.
simulator.podAnnotations
{}
podAnnotations: extra annotations on the simulator pod template, mirroring podAnnotations below but scoped to the simulator Deployment only.
simulator.nodeSelector
{}
nodeSelector: constrain simulator pods to nodes carrying these labels, mirroring nodeSelector below but scoped to the simulator Deployment only.
simulator.tolerations
[]
tolerations: let simulator pods schedule onto tainted nodes, mirroring tolerations below but scoped to the simulator Deployment only.
simulator.affinity
{}
affinity: pod/node affinity rules for the simulator pods, mirroring affinity below but scoped to the simulator Deployment only.
simulator.networkPolicy.extraEgress
[]
extraEgress lets an operator open one additional narrow hole (e.g. a split-horizon internal DNS server that is not kube-dns) without editing the template. Empty by default: the harness is everything the sandbox needs, by design.
receiver
Receiver tier (#109, docs/archive/plans/2026-09-28-receiver-tier.md): an OTLP/HTTP Alloy that a Gateway API gateway fronts. The gateway's HTTPRoute strips the tenant-route prefix and injects the tenant header; with pass-through tenancy the receiver forwards each request's data carrying that header to the tenant-aware backends below. The prefix is an identifier, not an authorizer -- see docs/gateway-tier-plan.md §3. OFF by default: an opt-in feature. Review gate R3 (docs/gateway-tier-plan.md §7: containment, a tested off-switch, the gateway as the only ingress, a real-Alloy tenancy proof) was signed 2026-09-29 with the receiver kept opt-in. With it on, Shepherd applies each tenant route's HTTPRoute itself (applyTenantRoutes below) and shows on the Tenant routes page whether the gateway attached it.
Key
Default
Description
receiver.enabled
false
enabled: deploy the receiver tier (opt-in -- see above).
receiver.replicas
1
replicas: receiver pods behind the receiver Service. Each is stateless.
receiver.applyTenantRoutes
true
applyTenantRoutes: Shepherd creates, updates and removes each tenant route's HTTPRoute (in this namespace, pointing at the receiver Service) and shows whether the gateway attached it. Grants Shepherd's ServiceAccount a namespaced Role on httproutes and a ClusterRole to read the one HTTPRoute CRD, and mounts its token. false: no RBAC; write the HTTPRoutes yourself.
receiver.publicBaseURL
""
publicBaseURL: your gateway's public https URL (e.g. https://telemetry.example.com) -- where apps outside the cluster send OTLP. Prefills the "Connect an app" snippets on the Tenant routes page; users can still enter a different one there. Empty: they type it each time.
receiver.mode
pass_through
mode: "pass_through" forwards the tenant header the gateway injected (one receiver serves many tenants); "static" sends no tenant header. Pass-through refuses a gRPC listener, so only OTLP/HTTP is ever served.
receiver.listener.port
4318
port: the OTLP/HTTP port the receiver listens on and its Service exposes.
receiver.listener.maxRequestBodySize
"8MiB"
maxRequestBodySize: the largest request body accepted. Required -- there is deliberately no implicit limit.
receiver.batch.timeout
"5s"
timeout: how long a batch may wait before it is sent.
receiver.batch.sendBatchSize
8192
sendBatchSize: items that trigger sending a batch.
receiver.batch.sendBatchMaxSize
0
sendBatchMaxSize: the largest batch sent; 0 means no cap. A non-zero value below sendBatchSize is refused at render.
receiver.exporters.metrics.enabled
false
enabled: forward metrics (e.g. to Mimir's OTLP endpoint).
receiver.exporters.metrics.protocol
http
protocol: "http" (otlphttp) or "grpc" (otlp) toward the backend. This is the outbound connection; the listener stays OTLP/HTTP.
gatewayFrom: NetworkPolicy peers matching your gateway's data-plane pods -- the ONLY ingress the receiver accepts. Required when enabled, and every peer needs a non-empty podSelector (a namespace alone would admit any pod in it, and any such pod could assert any tenant). Gateway API implementations label their data plane gateway.networking.k8s.io/gateway-name: <gateway>, e.g. - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: gateways}} podSelector: {matchLabels: {gateway.networking.k8s.io/gateway-name: edge}}
receiver.networkPolicy.egress
[]
egress: NetworkPolicy egress rules reaching your backends (ipBlock, or namespace/pod selectors for in-cluster ones). Required when enabled: egress is default-deny apart from cluster DNS.
receiver.networkPolicy.dns.podSelector.k8s-app
kube-dns
k8s-app: CoreDNS's label in most distributions (kind, kubeadm, EKS, GKE).
receiver.storageSizeLimit
256Mi
storageSizeLimit: size cap on Alloy's scratch storage (an emptyDir; the root filesystem is read-only).
receiver.resources.requests.cpu
100m
cpu: minimum CPU reserved for the receiver.
receiver.resources.requests.memory
128Mi
memory: minimum memory reserved for the receiver.
receiver.resources.limits.cpu
"1"
cpu: the receiver's CPU ceiling.
receiver.resources.limits.memory
512Mi
memory: the receiver's memory ceiling.
receiver.podAnnotations
{}
podAnnotations: extra annotations on the receiver pod template.
receiver.nodeSelector
{}
nodeSelector: constrain receiver pods to nodes carrying these labels.
receiver.tolerations
[]
tolerations: let receiver pods schedule onto tainted nodes.
receiver.affinity
{}
affinity: pod/node affinity rules for the receiver pods.