How-to guide
Declarative ticket integrations
Configure your GitHub, Jira, GitLab, and Linear ticket targets from a version-controlled manifest instead of the UI — with credentials supplied by reference (never in the file) from your platform's secret store. Built for GitOps, immutable deployments, and managed secret rotation.
Ticket targets — the GitHub, Jira, GitLab, and Linear destinations Workbench opens tickets in — can be configured two ways. The interactive path is the Integrations settings page: an operator fills in a form and pastes a credential. The declarative path is a version-controlled manifest applied at deployment time, where the credential is a reference to a mounted secret, never a token pasted into a file.
This page covers the declarative path: the manifest schema, how a
credential is supplied by reference (secret_ref) from a
platform-managed secret store, and how the two paths coexist.
The declarative path exists for the same reasons GitOps does: repeatable configuration, immutable deployments, secret rotation owned by the platform, and a clean separation of duties — the person who writes the manifest never handles the token.
When to use declarative config
| Interactive (UI) | Declarative (manifest) | |
|---|---|---|
| Configured by | an operator, in the browser | a manifest applied at deploy time |
| Credential | pasted token, encrypted at rest by Workbench | a secret_ref to a secret the platform mounts |
| Best for | a quick first target, ad-hoc changes | GitOps, immutable/reproducible deployments, managed rotation |
| Rotation | operator re-enters the token | the platform rotates the mounted secret |
The two are not exclusive — a deployment can have UI-created targets and manifest-managed targets side by side. Manifest-managed targets are marked as externally managed in the UI (see Coexisting with the UI).
The manifest
A manifest is a YAML (or JSON — YAML is a superset) document listing the
targets to configure. It declares a schema version and an
integrations array:
version: 1
integrations:
- platform: github # github | jira | gitlab | linear
name: Production findings
base_url: https://api.github.com # GitHub Enterprise: https://ghe.example.com/api/v3
target_identity: acme/findings # owner/repo (GitHub), project key (Jira), project path/id (GitLab), team key (Linear)
credential_mode: pat # see the per-platform setup docs
secret_ref: file:///run/secrets/github-token
platform_options: {} # per-platform non-secret options
auto_dispatch_min_severity: off # off | low | medium | high | critical
Fields
| Field | Required | Notes |
|---|---|---|
platform | yes | github, jira, gitlab, or linear. |
name | yes | Display name for the target. |
base_url | yes | API base URL. Public host, or your self-managed / Enterprise host. Linear is fixed to https://api.linear.app/graphql. |
target_identity | yes | owner/repo (GitHub), project key (Jira), numeric id or namespace path (GitLab), team key (Linear — uppercase, e.g. ELE). |
credential_mode | yes | The authentication mode — the value the platform's setup doc lists (e.g. pat, app_installation, api_token, project_token; Linear: personal_api_key or oauth_bearer). |
secret_ref | yes | A reference to the mounted credential. The only place a credential may come from in a manifest. |
platform_options | no | Non-secret per-platform options (e.g. the Jira account email, GitHub App app_id / installation_id, Linear project_id / state_id / assignee_id / cycle_id / extra_label_ids). |
auto_dispatch_min_severity | no | Threshold that arms automatic dispatch; defaults to off. |
⚠️ A manifest never contains a secret. Inline credential fields —
credential, token, password, private_key, api_token — are a
hard validation error. The credential is always a secret_ref. This
keeps tokens out of version control and out of the manifest's blast
radius entirely.
External secret references
A secret_ref points at a credential the platform mounts as a file.
The launch scheme is file:// with an absolute path:
secret_ref: file:///run/secrets/github-token
Workbench reads the referenced file lazily, at the moment it needs the credential — never at apply time, never into the database, never into a log, an error message, or an audit event. The stored target carries only the reference (a non-secret string), so the secret's lifecycle is entirely the platform's to own.
The contract is deliberately provider-neutral: Workbench consumes a mounted file and nothing else. It embeds no cloud SDKs and holds no cloud IAM credentials. Any secret store that can present a secret as a file works — which, via the Kubernetes Secrets Store CSI driver, covers AWS Secrets Manager, Azure Key Vault, and Google Secret Manager, plus Docker secrets and systemd credentials outside Kubernetes.
Mounting the secret
The examples below all end the same way: the secret lands at a path
inside the container, and the manifest's secret_ref is
file://<that path>.
AWS Secrets Manager (Kubernetes Secrets Store CSI)
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: workbench-github-token
spec:
provider: aws
parameters:
objects: |
- objectName: "prod/workbench/github-token"
objectType: "secretsmanager"
Mount it into the Workbench pod and reference the mount path:
volumes:
- name: tokens
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: workbench-github-token
volumeMounts:
- name: tokens
mountPath: /mnt/secrets-store
readOnly: true
# secret_ref: file:///mnt/secrets-store/github-token
The CSI driver authenticates to AWS with the pod's IAM role (IRSA) — the cloud credential never enters Workbench.
Azure Key Vault (Kubernetes Secrets Store CSI)
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: workbench-github-token
spec:
provider: azure
parameters:
keyvaultName: "workbench-kv"
tenantId: "00000000-0000-0000-0000-000000000000"
objects: |
array:
- |
objectName: github-token
objectType: secret
# mount at /mnt/secrets-store; secret_ref: file:///mnt/secrets-store/github-token
Workload identity on the pod authenticates to Key Vault.
Google Secret Manager (Kubernetes Secrets Store CSI)
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: workbench-github-token
spec:
provider: gcp
parameters:
secrets: |
- resourceName: "projects/my-project/secrets/workbench-github-token/versions/latest"
path: "github-token"
# mount at /mnt/secrets-store; secret_ref: file:///mnt/secrets-store/github-token
Workload Identity on the pod authenticates to Secret Manager.
Docker secrets
printf '%s' "$GITHUB_TOKEN" | docker secret create github-token -
services:
workbench:
secrets:
- github-token
secrets:
github-token:
external: true
# mounted at /run/secrets/github-token; secret_ref: file:///run/secrets/github-token
systemd credentials
[Service]
LoadCredential=github-token:/etc/workbench/secrets/github-token
systemd exposes the credential at
$CREDENTIALS_DIRECTORY/github-token (typically
/run/credentials/workbench.service/github-token):
secret_ref: file:///run/credentials/workbench.service/github-token
Applying a manifest
Point Workbench at the manifest with WORKBENCH_INTEGRATIONS_MANIFEST:
WORKBENCH_INTEGRATIONS_MANIFEST=/etc/workbench/integrations.yaml
Workbench reconciles the manifest on start. Apply is idempotent and order-independent: a target is keyed by its organization, platform, normalized base URL, and target identity, so
- a target in the manifest that does not exist yet is created;
- a target that already exists is updated in place to match;
- re-applying an unchanged manifest is a no-op.
Running the same manifest against ten identical deployments produces ten
identical configurations. A malformed manifest (bad schema version, an
inline secret, an invalid secret_ref) fails closed with a specific
reason and changes nothing — Workbench does not partially apply.
Reconciliation is additive and updating; it does not delete. A target you remove from the manifest is left in place, not torn down — deleting a target is a deliberate action through the UI or API, so a manifest edit can never silently drop a live integration. Prune intentionally.
Rotation
Because the credential is a mounted file, rotation is the platform's job, not Workbench's. The secret store (or your rotation job) writes the new value to the same mount; Workbench reads the current contents on the next dispatch, so a rotated token is picked up without reconfiguring anything in Workbench. No restart is required for a value change at an existing path.
Changing the reference itself (a new path, a new target) is a manifest change — edit the manifest and re-apply.
Coexisting with the UI and declarative config
A manifest-managed target is visible on the Integrations page like
any other, but is clearly marked as externally managed and shows its
non-secret source (the secret_ref). There is no secret to display —
there is none stored.
Because the manifest is the source of truth for the targets it lists, a UI edit to a manifest-managed target is overwritten on the next reconcile. The UI surfaces this so the change is deliberate: manage those targets through the manifest, and use the UI for interactively-created targets. Deletion is always explicit (UI/API); reconciliation never deletes.
Plan availability
Ticket integrations — interactive and declarative alike — are a Professional, Business, and Enterprise capability, subject to the signed entitlement's allowed platforms and target cap. On Starter they are not included: a manifest applied on a Starter deployment configures nothing, exactly as the Integrations page is unavailable there. Declarative configuration is another way to reach the same capability, never a way around the entitlement.
Security model
The external-secret path is built to keep a mounted credential inside its mount and out of every output surface:
- Path traversal — a
secret_refmust be an absolutefile://path. Relative paths and..segments are rejected before any filesystem access. - Symlink escape — the final path component is opened without following a symlink, so a mount cannot be swapped for a link pointing outside it.
- TOCTOU — the path is resolved and opened in one step and the opened descriptor is re-checked to be a regular file, bounding the window between check and use.
- Redaction — the secret bytes are never persisted, logged, returned in a response, or written to an audit event. Error messages carry the non-secret reference path only.
- Provider unavailable — an absent or unreadable mount is a clean, retryable failure that leaves the stored configuration intact; the target's connection test reports it so an operator can act on it.
Mount permissions are the platform's responsibility. A world- or group-readable mount is a warning, not a hard failure — tighten it at the mount, where it belongs.
Next
- Security posture — Workbench's overall security posture and the master key that protects UI-stored credentials.
- Database registration — the other half of onboarding: the databases whose findings become tickets.