Documentation menu

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:

OptionEnvironmentansible.cfgDefault
api_keySS_API_KEY[secretserver] api_keyrequired
api_urlSS_API_URL[secretserver] api_urlhttps://api.secretserver.io
ca_pathSS_CA_PATH[secretserver] ca_pathsystem roots
timeoutnonenone10 (seconds)
versionnonenonenot set
rendernonenonefalse

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.

Lookup results are not hidden automatically. Put 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: true

Module 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: true

Error handling

Failures raise AnsibleError, which fails the task. Messages include the HTTP status but never the response body.

MessageCause
SecretServer API key is requiredNo api_key, SS_API_KEY or ansible.cfg entry
... (HTTP 401 / 403 / 404)Bad key, missing scope, or unknown path
SecretServer connection failedDNS, TLS or network error
api_url must use httpsPlain http to a non-loopback host
version must be between 1 and 12Out-of-range version
Historical reads require container/name/versionVersion requested on a bare name
response contains no scalar secretThe 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: true

Complete 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: true

Self-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') }}"