Clients & Libraries
Ansible
A lookup plugin that reads secret values and resolves %%NAME%% templates inside playbooks. It uses only the Python standard library (urllib).
Install
Copy the standalone lookup from the clients repository into a lookup_plugins/ directory next to your playbook. It is then available as lookup('secretserver', ...).
git clone https://github.com/afterdarksys/secretserver-clients.git mkdir -p lookup_plugins cp secretserver-clients/ansible/secretserver.py lookup_plugins/
The same lookup also ships in the afterdark.secretserver collection together with a secret module for writes. The collection is built from the ansible/ directory of the SecretServer server repository:
ansible-galaxy collection build ansible --output-path /tmp/secretserver-release ansible-galaxy collection install /tmp/secretserver-release/afterdark-secretserver-1.1.0.tar.gz
With the collection installed, call the lookup as afterdark.secretserver.secretserver.
Authenticate
Each option can come from a lookup keyword, an environment variable or ansible.cfg:
| Option | Environment | ansible.cfg | Default |
|---|---|---|---|
api_key | SS_API_KEY | [secretserver] api_key | required |
api_url | SS_API_URL | [secretserver] api_url | https://api.secretserver.io |
ca_path | SS_CA_PATH | [secretserver] ca_path | system roots |
timeout | none | none | 10 (seconds) |
version | none | none | not set |
render | none | none | false |
Keep the key in an encrypted variable file (Ansible's own vault feature) or your controller's credential store, and pass it as api_key=.
Read a secret
vars:
# container/key
db_password: "{{ lookup('secretserver', 'production/db-password') }}"
# previous version (1 = current, up to 12)
old_password: "{{ lookup('secretserver', 'production/db-password/2') }}"
# bare name lookup (current version only)
token: "{{ lookup('secretserver', 'stripe-key') }}"
# named variable template
sudo_line: "{{ lookup('secretserver', 'sudo_password=%%LOG_SERVER_TX1_S%%') }}"A term containing %% (or any term with render=true) is sent to POST /api/v1/variables/resolve instead of being treated as a path. Historical reads need the full container/name/version form.
no_log: true on every task that consumes a secret.Write a secret
The lookup is read-only. Use the afterdark.secretserver.secret module from the collection. It supports state: present|absent, check mode and idempotence, and never returns the value. It manages the whole record, so omitted metadata (description, container, tags) is cleared.
- name: Store a generic secret
afterdark.secretserver.secret:
api_url: https://api.secretserver.io
api_key: "{{ vault_secretserver_api_key }}"
name: database-password
value: "{{ vault_database_password }}"
container_id: "{{ production_container_uuid }}"
tags: [prod]
state: present
no_log: trueModule options: api_url, api_key, name, value, description, container_id, tags, state, validate_certs (must stay true), ca_path, timeout. It returns the secret id.
List secrets
Neither the lookup nor the module lists secrets. Call the REST route with ansible.builtin.uri:
- name: List secret names
ansible.builtin.uri:
url: https://api.secretserver.io/api/v1/secrets
headers:
Authorization: "Bearer {{ vault_secretserver_api_key }}"
return_content: true
register: listing
no_log: trueError handling
Failures raise AnsibleError, which fails the task. Messages include the HTTP status but never the response body.
| Message | Cause |
|---|---|
| SecretServer API key is required | No api_key, SS_API_KEY or ansible.cfg entry |
| ... (HTTP 401 / 403 / 404) | Bad key, missing scope, or unknown path |
| SecretServer connection failed | DNS, TLS or network error |
| api_url must use https | Plain http to a non-loopback host |
| version must be between 1 and 12 | Out-of-range version |
| Historical reads require container/name/version | Version requested on a bare name |
| response contains no scalar secret | The record has no supported string field |
- name: Read an optional secret
ansible.builtin.set_fact:
feature_token: "{{ lookup('secretserver', 'production/feature-token') }}"
ignore_errors: true
register: token_read
no_log: trueComplete playbook
deploy.yml
- name: Deploy application
hosts: webservers
vars:
ss_key: "{{ vault_secretserver_api_key }}"
tasks:
- name: Fetch database password
ansible.builtin.set_fact:
db_password: "{{ lookup('secretserver', 'production/db-password', api_key=ss_key) }}"
no_log: true
- name: Write database config
ansible.builtin.template:
src: db.conf.j2
dest: /etc/app/db.conf
mode: "0600"
no_log: trueSelf-hosted server with a private CA
db_password: "{{ lookup('secretserver', 'production/db-password',
api_url='https://secrets.internal.example',
ca_path='/etc/ssl/private-ca.pem') }}"