Environment Config & Secrets
Every environment carries the configuration your tests need at run time. There are three distinct places a value can live, and picking the right one keeps tests portable and secrets safe. Tests only ever reference one of them — properties — so this page starts there.
| Where it lives | Referenced in a test as | Stored in | Readable back? | Use it for |
|---|---|---|---|---|
| Property | {{propertyName}} | Proofarc DB | yes (unless marked secret) | Non-secret config: base URLs, page sizes, feature flags |
| Secret | (never directly — see below) | Kubernetes | no | Values that must never touch our database |
| Credential | credentialTag (auth only) | Proofarc DB (encrypted) | no (masked) | Login/OAuth/API-key auth for a target |
The one rule that ties it together: a test reads from exactly one place — properties. A secret is pulled into a property; the test never names the secret. This gives you a single, predictable source of truth for what a test resolves.
1. Properties — the values your tests read
A property is a named value on an environment. Reference it anywhere in an API scenario or UI test
with {{propertyName}}:
baseUrl: "{{apiBaseUrl}}"
steps:
- name: List users
endpointPath: "/api/users?pageSize={{pageSize}}"
Set them in the UI (Environments → expand an environment → Test Properties) or via API/MCP:
# API — the body is a raw list of {key,value}
POST /api/environments/{id}/variables
[{"key":"apiBaseUrl","value":"https://api.dev.example.com"},
{"key":"pageSize","value":"20"}]
# MCP
set_env_variables(environment="development",
variables={"apiBaseUrl":"https://api.dev.example.com","pageSize":"20"})
Properties resolve at the lowest precedence — a credential, the resolved target baseUrl, a
step extraction, or a hook output with the same name all win over a property. A property can also
drive the host with no target row: author base_url: "{{apiBaseUrl}}" and set that property.
A property can be marked secret (isSecret) to encrypt it at rest and mask it in the UI/logs —
but for values that must never live in our database at all, use a Kubernetes secret instead.
2. Secrets — Kubernetes-backed, write-only
Secrets are stored in Kubernetes, never in the Proofarc database, and are write-only: once set, the value can never be read back through the API or UI (GitHub-style semantics). You manage them per environment; internally each environment maps to one Kubernetes Secret object keyed by a stable environment id, so two environments both named "prod" never collide.
Managing secret values requires the SUPER_ADMIN role. ADMIN/ANALYST can see the secret
names (to know which references exist) but cannot set or read values.
How a test uses a secret
A test never references {{secrets.X}} directly. Instead:
- Set the secret (SUPER_ADMIN):
POST /api/environments/{id}/secrets # SUPER_ADMIN{"key":"CLIENT_SECRET","value":"s3cr3t-value"}
- Reference it from a property value as
{{secrets.NAME}}:POST /api/environments/{id}/variables[{"key":"clientSecret","value":"{{secrets.CLIENT_SECRET}}"}] - The test uses the property —
{{clientSecret}}— and knows nothing about the secret.
At run time, while the environment's properties are seeded into scope, any {{secrets.NAME}} in a
property value is resolved against that environment's Kubernetes secret. The resolved value is then
treated as secret-derived and automatically masked in logs, results, and execution snapshots —
so it never surfaces even though the test consumed it.
test → {{clientSecret}} → property value {{secrets.CLIENT_SECRET}} → Kubernetes Secret
(the only ref) (resolved while seeding properties) (write-only, per env)
Rotating and deleting
- Rotate by setting the same name again with a new value. Rotation updates the one key and preserves every other secret on the environment.
- Delete removes the key (and the whole Secret object once its last key is gone). After a
delete, any property whose value is
{{secrets.<name>}}will fail to resolve — the run fails loud rather than sending an unresolved placeholder.
Fail-loud behavior
If a property references {{secrets.X}} but the secret is unset, or the secrets store is disabled
on the deployment, the run fails loudly: the property is dropped, the {{propertyName}} survives
unresolved, and the standard unresolved-placeholder guard stops the step. A literal placeholder is
never sent to the target.
Managing secrets
UI — Environments → expand an environment → Secrets. You'll see the secret names, a
copy-the-reference button ({{secrets.NAME}}), and — for a SUPER_ADMIN — Add / Rotate / Delete.
Values are write-only; the dialog never shows an existing value.
MCP — three tools (set/delete are SUPER_ADMIN):
| Tool | Does |
|---|---|
list_environment_secrets(environment) | List names + whether the store is enabled (never values) |
set_environment_secret(environment, key, value) | Create or rotate a secret |
delete_environment_secret(environment, key, confirm=true) | Delete a secret |
API — GET / POST / DELETE /api/environments/{id}/secrets.
Operator note: the Kubernetes secrets store is off by default (
backend.secretsStore.enabled=false). Enabling it creates a namespaced Role granting the backendget/create/update/deleteon Secrets in its own namespace only. Until it's enabled,list_environment_secretsreportsenabled:falseand set/delete fail loud.
3. Credentials — auth for a target
Credentials are for authentication — logging a test into the target API/app. They live in the
environment's encrypted credential vault and are referenced by credentialTag, not by {{...}}.
See Access & Credentials and the API-testing
Authentication guide.
Credentials remain the right home for username/password, OAuth client secrets used for auth, and API keys that authenticate a scan or scenario. Kubernetes secrets (§2) are for arbitrary values you want out of our database entirely and surfaced through a property.
4. Global properties — instance-wide shared values
Global properties are a standalone, instance-wide store — not tied to any project or environment. Use them for values every test on the platform shares: constants, expected messages, shared test data, locator maps.
- UI: Configuration → Global Properties (New Property → key / value / description / secret).
- MCP:
get_global_properties,set_global_property,delete_global_property. - Reference them exactly like env properties:
{{myKey}}. They resolve on every test surface (API scenarios, mobile YAML, cloud-provider config). - Groups: a global whose value is a JSON object is a group — reference leaves by path:
{{checkout.submitButton}}reads thesubmitButtonfield of thecheckoutglobal. Path references resolve in mobile YAML and cloud-provider config; API scenarios currently resolve flat keys only ({{myKey}}). - Precedence — globals are always lowest. An environment property, credential, hook output, or step extraction with the same name overrides a global of that name. Most-specific wins; a global can never shadow environment-level config.
secret: truemasks the value in list responses, same as env properties.
# create / update (POST is an upsert on `key`)
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
$CSS_URL/api/global-properties \
-d '{"key":"supportEmail","value":"qa@example.com","description":"shared assertion value"}'
Which do I use?
- Non-secret config a test reads (URLs, sizes, flags) → property.
- A value that must never live in Proofarc's database, surfaced to a test → Kubernetes secret, referenced from a property.
- Authenticating a test/scan into a target → credential (
credentialTag). - The same value shared by tests across all projects/environments → global property (env property of the same name overrides it where they differ).