Secrets
Give a managed gateway the API tokens, keys, and passwords its config must not carry. Register a value per gateway with mcpg cloud secret set, reference it as ${secret.NAME}, and rotate it with one command — no publish, no restart, and the platform never shows a value back.
A published config is the whole gateway: every backend, every plugin, every
credential those need. Most of those credentials must not sit in a file you
version and hand around, and a managed gateway cannot read your shell
environment the way a gateway on your own laptop can — the pod's environment
belongs to the platform, so publishing refuses
${env.…} outright.
A secret is the answer: a named value you register for one gateway, which
the platform delivers to that gateway and nothing else, and which the config
references by name as ${secret.NAME}. The value is encrypted at rest, never
returned by any API, and reaches the gateway as a file it reads at config load
— not as an environment variable.
Register a secret, reference it from the config
Secrets are registered per gateway name, in the org, workspace, and environment your context names. Register before or after the first publish — the order does not matter, the publish that references the key is what needs it.
# the value comes from stdin (preferred), from an environment variable,
# or — least preferred — inline, where it lands in your shell history
printf '%s' "$GITHUB_TOKEN" | mcpg cloud secret set edge GITHUB_TOKEN
mcpg cloud secret set edge GITHUB_TOKEN --from-env GITHUB_TOKEN
mcpg cloud secret set edge GITHUB_TOKEN --value ghp_…
mcpg cloud secret list edge # keys, when, by whom — never a value
mcpg cloud secret unset edge GITHUB_TOKEN
A key is an identifier: letters, digits, and underscores, not starting with a
digit, at most 64 characters, case-sensitive — ^[A-Za-z_][A-Za-z0-9_]{0,63}$.
A value is at least one byte and at most 64 KiB, and it is stored exactly as
sent: the CLI strips the trailing newline a terminal appends to stdin (that is
the terminal's, not the secret's) and nothing else.
Reference the key from the config with ${secret.KEY}. It resolves anywhere a
string does — a field on its own, or inside a longer string:
plugins:
- name: github
kind: github
spec:
token: ${secret.GITHUB_TOKEN}
mcp:
capabilities:
tools:
- name: search
backend:
kind: http
url: https://api.example.com/search
headers:
authorization: "Bearer ${secret.SEARCH_API_KEY}"
Then publish as usual. Publishing checks every ${secret.KEY} against the keys
registered for that gateway name: a key that is not registered is refused
(422, violation unregistered_secret, naming the token) before anything is
provisioned, so a typo in the config or a forgotten secret set is caught at
publish time — never at the first call.
Route a secret through the config, not through the plugin's own
environment. A plugin that would read GITHUB_TOKEN from the process
environment on your laptop never sees a registered secret: on a managed gateway
the value is a file, and the config is the only thing that reads it. Put the
reference where the plugin's config takes the value (spec.token: ${secret.GITHUB_TOKEN}).
Live updates and rotation
On a running gateway, a secret set or secret unset is live within about a
minute — no publish, no restart. The platform rewrites the gateway's secret
volume, the gateway notices the change and reloads its config: every plugin
re-initialises with the new values, and client sessions survive the reload.
Before the gateway's first publish there is nothing to deliver to yet; the value is stored, and the first publish carries it.
Rotating a leaked or expired credential under incident is therefore one command:
printf '%s' "$NEW_TOKEN" | mcpg cloud secret set edge GITHUB_TOKEN
# ✓ edge: GITHUB_TOKEN set — live (the gateway reloads within about a minute)
The gateway reloads with the new value. The config does not change, no new
version is recorded, and no pod restarts. A
value that a config referenced and that you unset is a config-load error on
the next reload — the gateway keeps the previous config live and reports the
key name — so unset a key after the config has stopped referencing it.
Each set and unset reports how the change was delivered:
| Delivery | Meaning |
|---|---|
live | Pushed to the running gateway; it picks the value up within about a minute. |
pending_publish | The gateway is not provisioned yet. The value is stored and the next publish carries it. |
failed | The value is stored, but the push to the running gateway was refused; the message carries the reason. Retry the command, or publish the current config to carry it. |
And secret list reports one state for the set as a whole. The gateway reports
a fingerprint of the secrets it loaded; the platform compares it with the set
you registered. A fingerprint crosses, never a value.
| State | Meaning |
|---|---|
live | The gateway has loaded the current registered values. |
pending | The gateway has not loaded the current registered values yet — a change made moments ago, or a gateway that has not been published since. |
unknown | The platform has no report yet: the gateway is not provisioned, or has not enrolled. |
What the platform never does
- Return a value. No API, no CLI command, no console view, no log line shows a secret once it is set. A listing is keys and bookkeeping. If you need to know what a secret is, you need the source you set it from.
- Capture a value in a config version. A version records the config, which holds the reference and not the value; a rollback keeps the secrets that are registered now. Rotating a secret is not a config change and records no version.
- Share a secret between gateways. A secret belongs to one gateway name in
one org, workspace, and environment.
GITHUB_TOKENonedgeandGITHUB_TOKENonstagingare two secrets, registered twice. - Put a secret in the environment. The value is a file the gateway reads at
config load, so
${env.…},env://, andfile://stay refused for every published config, and a plugin's own environment-variable discovery never finds it. - Store it in the clear. Values are encrypted at rest under the platform's
key, bound to the gateway they were registered for, and every
setandunsetis audited by key name.
Secrets in CI
A deploy pipeline needs a member credential. A service token
(mcpg cloud service-token create, exported as MCPG_CLOUD_TOKEN) works for
secret set, secret list, secret unset, and publish alike, so the
credential that deploys a config is the one that supplies what the config
references.
Two questions look alike and are answered in different places:
-
Is the key well-formed? A key outside
^[A-Za-z_][A-Za-z0-9_]{0,63}$can never be registered, so a${secret.…}that names one can never be published. That is a property of the file, and it belongs in a pull-request check that greps the diff. -
Is the key registered? Only the platform knows. Ask it with a dry run against the live coordinates — it runs every gate a publish runs and reserves nothing:
bashmcpg cloud publish edge --config gw.yaml --dry-run # ✓ 'edge' would update in place (2 replica(s), size s, gateway …) # config sha256: … # secrets: GITHUB_TOKEN, SEARCH_API_KEY # nothing was reserved or provisioned
Never keep a local list of registered keys — in a repository, a wiki, or a
pipeline variable. A list drifts from the thing it describes, and a drifted
list fails green. The platform is the list; secret list and the dry run read
it.
In the console
The gateway page in the control-plane console has a Secrets tab: the
registered keys with when and by whom each was last set, and one state badge
for the set (live / pending / unknown). Add or replace a secret with a key
and a write-only value field — the value is sent once and cleared from the
form — and remove one after a confirmation. Values are never shown, in this
tab or anywhere else.
Reference
| CLI | REST | Notes |
|---|---|---|
mcpg cloud secret set <gateway> KEY (stdin | --from-env VAR | --value V) | PUT …/gateways/{name}/secrets/{KEY} body {"value": "…"} | Set or replace. Reply carries delivery.state: live | pending_publish | failed. |
mcpg cloud secret list <gateway> | GET …/gateways/{name}/secrets | Keys with created_by / updated_at, plus the set state: live | pending | unknown. Never a value. |
mcpg cloud secret unset <gateway> KEY | DELETE …/gateways/{name}/secrets/{KEY} | Remove. Same delivery reply as set; 404 when the key is not registered. |
mcpg cloud publish <gateway> --config f --dry-run | POST …/gateways/validate | Prints secrets: … — the registered keys the config may reference. |
… is /v1/orgs/{org}/workspaces/{ws}/environments/{env}. The routes are
member-gated like publish. A malformed key is 422 invalid_secret_key, an empty
value 422 empty_secret_value, a value over 64 KiB 413 secret_value_too_large.
Where to go next
- Publish a config — the publish that references the secret.
- Versions and rollback — what a version records, and what it does not.
- Build a config in the browser — the wizard, and which secret form each destination uses.
- Configure the gateway — the config the reference lives in.