Configuration model¶
How gpm stores configuration: typed YAML objects in a git repository, one object per file, validated as a whole graph on every write.
go-proxy-manager is configured by a set of typed YAML objects in a git-backed
directory (default /data/config). You can edit them through the web UI / REST
API, or write the files directly and let the daemon load them on start/reload.
Layout¶
config/
settings.yaml # singleton app settings
dns-ledger.yaml # singleton DNS record-ownership ledger (reconciler-written)
access-list-sources.yaml # singleton fetched access-list source sets (fetcher-written)
proxy-hosts/<name>.yaml
redirect-hosts/<name>.yaml
stream-hosts/<name>.yaml
parked-hosts/<name>.yaml
certificates/<name>.yaml
client-cas/<name>.yaml
dns-providers/<name>.yaml
identity-providers/<name>.yaml
upstream-groups/<name>.yaml
access-lists/<name>.yaml
middlewares/<name>.yaml
api-tokens/<name>.yaml
One object per file; the file's base name must equal the object's name. The
directory is a git repository: every change made through the API is a commit,
and the whole graph is validated before it is accepted (a reference to a
non-existent certificate, middleware, access list, identity provider, or DNS
provider is a load-time error, and an object cannot be deleted while another
references it).
Unknown keys¶
Both loaders (per-object files and settings.yaml) decode leniently: a key the
running binary's struct does not recognise (most often one only a newer
gpm writes) is silently ignored rather than rejected, so a config a newer
release wrote still loads on an older one instead of failing outright. Load
logs one WARN per affected file and lists it under GET /health's
configWarnings. Silently ignored is also silently dropped the moment
that object is next written (including by an automatic reconciler commit, not
only an operator save), which makes an unsupported rollback the one case this
leniency does not cover cleanly; see Rollback.
Common fields (every object)¶
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Identity and filename. Lowercase alphanumeric plus -_., must start and end alphanumeric, 1-254 chars. |
displayName |
string | no | Human label for the UI. |
labels |
map | no | Arbitrary key/value metadata. gpm.rake.pro/managed-by is reserved: see below. (The exact key follows ingressDiscovery.annotationPrefix; gpm.rake.pro is the default and what every example below uses.) |
tags |
[]string | no | Flat, free-form labels for grouping/filtering. On the Proxy Hosts list they render as chips and are matched by the filter box. |
disabled |
bool | no | Keep the object in config but exclude it from the running proxy listeners. |
gpm.rake.pro/managed-byis a reserved label: do not set it by hand. It marks an object as owned by an automated reconciler. Addinggpm.rake.pro/managed-by: ingress-discovery(ordocker-discovery) to a proxy host you wrote yourself hands it to that reconciler, which will delete it on the next poll, because no annotatedIngress(or labelled container) derives it. Removing the label is the supported way to adopt a discovered host permanently; adding it is never the way to give one away.A managed host is not editable by hand: every edit besides
disabledandmaintenanceis reverted on the next poll. Discovery derives the whole object from the template and theIngressand writes it back whenever it differs from what is stored, so an editeddisplayName, addedtags,timeouts,locationsorrobotsNoIndexall survive at most until the next reconcile (default 60s).
disabled: trueis the exception: it is operator-owned state. Discovery honours an operator-setdisabledand never clears it itself: hand-disabling a managed host (in the UI or by editingconfig/proxy-hosts/<name>.yaml) survives every subsequent poll, keeps the object out of the running proxy listeners, and withdraws its DNS records exactly like disabling a hand-written host does. Editing the Ingress cannot undo this: a cluster user has no way to re-enable a host you disabled. The one case where adisabled: truea poll wrote itself IS cleared automatically is the fail-closed hold below (an unresolvable profile): that disable is discovery's own, and the very next reconcile that resolves the profile again lifts it. The labelgpm.rake.pro/disabled-by: ingress-discoveryon the stored object is how the two are told apart: never set or remove it by hand; it exists only for discovery to recognise a hold it placed itself.
maintenance: trueis operator-owned in exactly the same way. No Ingress annotation derives it, so discovery carries the stored value forward on every reconcile instead of resetting it: a managed host put into maintenance (in the UI or by editingconfig/proxy-hosts/<name>.yaml) stays in maintenance until an operator takes it out, whatever the cluster does to theIngressin the meantime. Unlikedisabledit keeps the host's domains, certificate and DNS records: see Maintenance mode.With that, there is no longer only an "emergency" off-switch (disabling in the UI now works), but the other two routes remain useful:
- Remove
gpm.rake.pro/managedfrom theIngress(or delete theIngress). Discovery stops deriving the host and deletes it on the next successful reconcile. This is the clean route for taking a service out of discovery for good, but it needs cluster access.- Remove the
gpm.rake.pro/managed-bylabel from the proxy host. The object becomes operator-authored, discovery refuses to touch it ever again, and you can then edit it freely. This still needs no cluster access, and is still permanent: the correspondingIngressis skipped with a warning from then on, and putting the host back under discovery means deleting it and letting the next poll recreate it.Preview what a reconcile would do to a specific host with
GET /api/ingress-discovery/plan(or Preview changes in the settings UI) before disabling by hand, if in doubt.
Domains are exclusive¶
The proxy listeners route by hostname, so at most one enabled host may claim a
given domain. Two enabled proxy, redirect or parked hosts listing the same domain
are rejected at load time (hosts "a" and "b" both claim domain "x.example.com")
rather than resolved by whichever file happens to be read last. Disabled hosts
are exempt: they are excluded from the running proxy listeners entirely, so staging a
replacement host beside the live one stays legal: enable the new one in the same
change that disables the old one.
Secrets¶
Secret-valued fields (API tokens, client secrets, etc.) must be placeholders, not literal values:
${ENV:CF_API_TOKEN} # resolved from the environment variable
${FILE:/run/secrets/token} # resolved from a file (e.g. a Docker secret), trimmed
Placeholders resolve lazily, at the moment the secret is used. Committing a
literal secret is refused with refusing to commit literal secret(s): ..., on
Save, on SaveSettings, and on Restore (an uploaded backup archive cannot
smuggle a plaintext secret onto disk or into git history; a refused restore rolls
the working tree back and commits nothing). In API responses, literal secrets are
redacted to ***; placeholders are returned verbatim.
${FILE:...} reads are confined to an allowlisted root, defaulting to
/run/secrets. A path that is relative, or outside the allowed root (including
via ..), is refused, so a config write cannot turn a file-backed secret into
an arbitrary host-file read. Override the allowed roots with the
GPM_SECRET_FILE_ROOTS environment variable (a list of absolute directories,
separated by the OS path-list separator, e.g. : on Linux).
${ENV:...} resolution has two guards. gpm's own sensitive process env vars
(GPM_SSO_SIGNING_KEY, GPM_LOCAL_ADMIN_PASSWORD_HASH and
GPM_LOCAL_ADMIN_TOTP_SECRET) are never
resolvable via a ${ENV:...} placeholder, so an admin-authored config value
cannot exfiltrate them (e.g. as a webhook secret posted to an attacker URL). By
default any other env var name resolves. To lock this down further, set
GPM_SECRET_ENV_PREFIXES to a comma-separated list of allowed name prefixes
(e.g. GPM_SECRET_,APP_); then only ${ENV:...} names carrying one of those
prefixes resolve and everything else is refused.