BeyondTrust/POV

← Back to dashboard

Secrets in a Remote Worker run

Audience: operator · Profile: both · Read this when: a playbook needs a credential and you do not want it in the playbook.

Part of Remote Worker. Hardened per-provider lookups, Password Safe managed-account checkout, and in-playbook lookups.

Using a Secrets-Management secret in a run

Beyond the SSH key, a run can pull secrets from Secrets Management — a DB-stored secret or an external vault reference (aws_sm://, gcp_sm://, azure_kv://, bt_safe://) — without the operator ever seeing the value. The Use a secret panel on /config-mgmt offers three bindings:

Binding Becomes Runners
Named variable an extra var (-e) — redacted from job output local + cloud
Become / sudo password ansible_become_password (Ansible no_logs it) local + cloud
SSH private key the connection key (replaces the configured key) local + cloud

Using a secret requires the secrets:use permission (admins bypass; "Full access" includes it). The use is audited — kinds + var names only, never the source refs or values — and any resolved value is scrubbed from the job output.

Cloud runners: hardened per provider (and the store requirement)

On the cloud runners the value is not placed in the task's plaintext env or on the command line. Each secret is delivered through the provider's own secret channel, and the container decodes a non-secret manifest into a 0600 vars file before running ansible-playbook -e @file:

Runner Channel Requirement
ECS (AWS) container secrets → valueFrom (SM ARN); the execution role fetches it at launch secret must live in AWS Secrets Manager (aws_sm://…); role needs secretsmanager:GetSecretValue
Cloud Run (GCP) secret-env secret_key_ref (version: latest); the service account fetches it secret must live in GCP Secret Manager (gcp_sm://…); SA needs roles/secretmanager.secretAccessor
ACI (Azure) secure_value env (inline, hidden from the portal) any secret — the value is injected inline

Because ECS and Cloud Run reference a store secret rather than carrying its value, a variable/become secret used on those runners must already live in that cloud's store. If it doesn't, the run is rejected up front with an actionable message — move it there via Secrets → migrate, then reference it as aws_sm://<name> / gcp_sm://<name>. ACI has no such requirement. The SSH-key secret always rides the existing SSH_KEY_B64 channel and needs no migration.

Managed-account checkout (BeyondTrust Password Safe)

When Password Safe is enabled (password_safe_enabled), a run can also use a Password Safe managed account as the login identity — instead of referencing a stored secret, the operator picks an account from a live list and the dashboard checks out its credential just-in-time at run time. The operator never sees the value; the checkout is scrubbed from output and audited exactly like the secret path above (and needs the same secrets:use permission).

How to use it: on /config-mgmt, pick Target → On-prem host (IP / hostname) and enter a system registered in Password Safe (a cloud VM's IP works too). The dashboard looks up that host's managed systems + accounts and shows an account picker (each tagged [SSH key] or [password]). Selecting one:

Across many hosts (bulk runs). The picker above pins system_id and account_id, and both belong to one managed system — so the same reference cannot be reused across a fleet: it would check out a single machine's credential and connect to every host with it. Correct only if the account happens to be domain-linked, wrong for a local account, and nothing would report which. A bulk run therefore sends the account name, and each job resolves it against the host it is actually configuring before checking anything out. Consequences:

Not available for Kubernetes / database batches. Those run a localhost play with no SSH connection to authenticate, and the run path silently ignores the connection-identity fields. A single run can absorb that quietly; a batch would leave you believing a credential had been applied to every cluster — so managed_account, managed_become, secret_ssh_key_source and secret_become_source are refused outright for a non-VM batch. Named secret_vars are honored there and stay available.

Local and Azure (ACI) runners inject the credential inline — the local runner via a 0600 vars file, ACI via secure_value — so a checked-out managed account works on either out of the box.

ECS and Cloud Run reference a store secret (the task identity fetches it at launch), which a checked-out (ephemeral) credential has none of — so they're rejected unless one of two settings is on (Settings → Ansible):

SSH-password targets require sshpass in the runner image (already true for the built-in on-prem SSH path). The lookup and checkout go through ps-cli, authenticated by the configured Password Safe OAuth client (pscli_api_url / pscli_client_id / pscli_client_secret).

In-playbook Password Safe lookup (beyondtrust.secrets_safe)

The managed-account checkout above is out-of-band: the dashboard fetches the credential and injects it. The complementary pattern is an in-playbook lookup — the play fetches its own secrets from Password Safe at runtime via the beyondtrust.secrets_safe Galaxy collection's secrets_safe_lookup plugin (and the beyondtrust.password_safe management modules). Use it for app secrets, API tokens, or DB credentials a task consumes — as opposed to the connection credential, which the checkout path handles.

Ready-to-run starters live in examples/playbooks/password-safe/.

Several shipped samples support this optionally. Rather than only living in the dedicated demos, the plays that consume an app secret each declare an optional …_secret var — set it to a SECRET path (folder/title) and the value is fetched mid-run; leave it blank and the play behaves exactly as before:

Playbook Optional var
windows/win-create-local-admin.yml new_admin_password_secret
database/postgres-create-role.yml target_role_password_secret
database/mysql-create-user.yml target_user_password_secret
portainer/*.yml portainer_pat_secret

Two implementation notes that matter if you adapt the pattern:

A file secret needs no different call. retrieval_type='SECRET' covers all three Secrets Safe types — credential, text and file. The collection's library branches on the type itself and fetches the file body for you when it sees one, returning the file's contents as a string under the same folder/title path a text secret would use. So lookup-secret.yml above works unchanged against a file secret — a PEM chain, say:

ansible-playbook lookup-secret.yml \
  -e secret_path='Certificates/api-gateway-chain' \
  -e dest_file=/etc/ssl/private/api-gateway.pem

Two constraints bite the file type only:

Auto-injected credentials. The lookup runs on the Ansible controller (the runner container) and reads PASSWORD_SAFE_API_URL / PASSWORD_SAFE_CLIENT_ID / PASSWORD_SAFE_CLIENT_SECRET. When Password Safe is enabled (password_safe_enabled) and the ps-cli OAuth client is configured, the dashboard auto-injects those three env vars into every runner (Local, ECS, ACI, Cloud Run) — reusing the same pscli_api_url / pscli_client_id / pscli_client_secret config as the checkout path, so there's nothing extra to set per run.