Skip to main content

Ansible Collection

Ansible collection for managing secrets with Arcan. Includes a lookup plugin for fetching secrets and modules for creating/updating secrets and realms.

Installation

ansible-galaxy collection install getarcan.arcan

Or from source:

cd integrations/ansible
ansible-galaxy collection build
ansible-galaxy collection install getarcan-arcan-1.0.0.tar.gz

Authentication

All plugins accept arcan_url and arcan_token as parameters. You can also set them via environment variables:

export ARCAN_URL=https://arcan.internal:8443
export ARCAN_TOKEN=arc_your_token_here

For playbooks, store the token in Ansible Vault:

ansible-vault encrypt_string 'arc_your_token' --name 'vault_arcan_token'

Plugins

Lookup Plugin: getarcan.arcan.secret

Fetch secrets for use in playbooks and templates.

# Single secret
- name: Get database URL
debug:
msg: "{{ lookup('getarcan.arcan.secret', 'DATABASE_URL', realm='production', env='prod') }}"

# Multiple secrets
- name: Get credentials
debug:
msg: "{{ lookup('getarcan.arcan.secret', 'DB_HOST', 'DB_PORT', realm='production') }}"

# With explicit auth
- name: Get secret
debug:
msg: >-
{{ lookup('getarcan.arcan.secret', 'API_KEY',
realm='myapp',
arcan_url='https://arcan.internal:8443',
arcan_token=vault_arcan_token) }}

# Self-signed certificates
- name: Get secret (skip cert check)
debug:
msg: "{{ lookup('getarcan.arcan.secret', 'KEY', realm='myapp', validate_certs=false) }}"

Module: getarcan.arcan.arcan_secret

Create, update, or delete secrets.

# Create or update a secret
- name: Store database URL
getarcan.arcan.arcan_secret:
key: DATABASE_URL
value: "postgres://user:pass@host:5432/db"
realm: production
env: prod
state: present

# Delete a secret
- name: Remove old key
getarcan.arcan.arcan_secret:
key: OLD_API_KEY
realm: production
env: prod
state: absent

The module is idempotent -- it only reports changed: true when the secret value actually differs.

Module: getarcan.arcan.arcan_realm

Create or list realms.

# Create a realm
- name: Create production realm
getarcan.arcan.arcan_realm:
name: Production
slug: production
state: present

# List all realms
- name: List realms
getarcan.arcan.arcan_realm:
state: list
register: result

- debug:
var: result.realms

SSL / Self-Signed Certificates

Arcan generates a self-signed CA by default. To skip certificate verification:

# Lookup
"{{ lookup('getarcan.arcan.secret', 'KEY', realm='app', validate_certs=false) }}"

# Modules
- getarcan.arcan.arcan_secret:
key: MY_KEY
value: my_value
realm: app
validate_certs: false

For production, configure a trusted CA or add the Arcan CA to your system trust store.

Security

  • Use valid TLS certificates in production (not self-signed)
  • Store API tokens in Ansible Vault (not hardcoded)
  • Use read-only tokens (arcan token create --scopes read) for applications that only need to read secrets
  • Enable audit logging on the Arcan server to track all secret access

Real-World Examples

Deploy a web application with database credentials

This playbook fetches database and cache credentials from Arcan and deploys a Django application:

---
# deploy-webapp.yml
- name: Deploy web application
hosts: webservers
become: true
vars:
arcan_url: "{{ vault_arcan_url }}"
arcan_token: "{{ vault_arcan_token }}"
app_realm: "myapp"
app_env: "prod"

tasks:
- name: Fetch database credentials
set_fact:
db_url: "{{ lookup('getarcan.arcan.secret', 'DATABASE_URL', realm=app_realm, env=app_env) }}"
redis_url: "{{ lookup('getarcan.arcan.secret', 'REDIS_URL', realm=app_realm, env=app_env) }}"
secret_key: "{{ lookup('getarcan.arcan.secret', 'DJANGO_SECRET_KEY', realm=app_realm, env=app_env) }}"
no_log: true

- name: Deploy application config
template:
src: templates/app.env.j2
dest: /opt/myapp/.env
owner: myapp
group: myapp
mode: "0600"
notify: Restart myapp

- name: Ensure application is running
systemd:
name: myapp
state: started
enabled: true

handlers:
- name: Restart myapp
systemd:
name: myapp
state: restarted

Using the lookup plugin in a Jinja2 template

Create a config template that pulls secrets at render time:

{# templates/app.env.j2 #}
# Application configuration — managed by Ansible
# DO NOT EDIT MANUALLY

DATABASE_URL={{ db_url }}
REDIS_URL={{ redis_url }}
DJANGO_SECRET_KEY={{ secret_key }}
ALLOWED_HOSTS={{ ansible_hostname }}
DEBUG=false

For Nginx configs that need a TLS certificate path from Arcan:

{# templates/nginx-site.conf.j2 #}
server {
listen 443 ssl;
server_name {{ app_domain }};

ssl_certificate {{ lookup('getarcan.arcan.secret', 'TLS_CERT_PATH', realm=app_realm, env=app_env) }};
ssl_certificate_key {{ lookup('getarcan.arcan.secret', 'TLS_KEY_PATH', realm=app_realm, env=app_env) }};

location / {
proxy_pass http://127.0.0.1:8000;
}
}

Rotate a secret with the module

This playbook generates a new API key, stores it in Arcan, and restarts the dependent service:

---
# rotate-api-key.yml
- name: Rotate API key
hosts: localhost
connection: local
vars:
arcan_token: "{{ vault_arcan_token }}"

tasks:
- name: Generate a new API key
set_fact:
new_api_key: "{{ lookup('password', '/dev/null length=48 chars=ascii_letters,digits') }}"
no_log: true

- name: Store the new API key in Arcan
getarcan.arcan.arcan_secret:
key: PAYMENT_API_KEY
value: "{{ new_api_key }}"
realm: myapp
env: prod
state: present
arcan_token: "{{ arcan_token }}"
register: secret_result

- name: Show rotation result
debug:
msg: "API key rotated successfully (changed: {{ secret_result.changed }})"

- name: Restart services that use the rotated key
hosts: appservers
become: true
tasks:
- name: Restart payment service
systemd:
name: payment-service
state: restarted

Multi-role deployment with per-service secrets

This example demonstrates a production pattern where each service gets its own set of secrets from different realms, and a Jinja2 config template is rendered with those values:

---
# site.yml — deploy all services with Arcan secrets
- name: Deploy application stack
hosts: all
become: true
vars:
arcan_token: "{{ vault_arcan_token }}"
arcan_url: "https://arcan.internal:8443"

pre_tasks:
- name: Validate Arcan connectivity
uri:
url: "{{ arcan_url }}/api/v1/health"
validate_certs: true
status_code: 200
delegate_to: localhost
run_once: true

- name: Deploy API servers
hosts: api_servers
become: true
vars:
app_realm: myapp
app_env: prod

tasks:
- name: Fetch all API secrets from Arcan
set_fact:
db_url: "{{ lookup('getarcan.arcan.secret', 'DATABASE_URL', realm=app_realm, env=app_env) }}"
redis_url: "{{ lookup('getarcan.arcan.secret', 'REDIS_URL', realm=app_realm, env=app_env) }}"
stripe_key: "{{ lookup('getarcan.arcan.secret', 'STRIPE_SECRET', realm=app_realm, env=app_env) }}"
sentry_dsn: "{{ lookup('getarcan.arcan.secret', 'SENTRY_DSN', realm=app_realm, env=app_env) }}"
jwt_secret: "{{ lookup('getarcan.arcan.secret', 'JWT_SECRET', realm=app_realm, env=app_env) }}"
no_log: true

- name: Render application config from template
template:
src: templates/api-config.toml.j2
dest: /opt/myapp/config.toml
owner: myapp
group: myapp
mode: "0600"
backup: true
notify: Restart API

- name: Deploy systemd unit
template:
src: templates/myapp-api.service.j2
dest: /etc/systemd/system/myapp-api.service
notify:
- Reload systemd
- Restart API

- name: Ensure API is running
systemd:
name: myapp-api
state: started
enabled: true

handlers:
- name: Reload systemd
systemd:
daemon_reload: true

- name: Restart API
systemd:
name: myapp-api
state: restarted

- name: Deploy background workers
hosts: worker_servers
become: true
vars:
app_realm: myapp
app_env: prod

tasks:
- name: Fetch worker secrets from Arcan
set_fact:
db_url: "{{ lookup('getarcan.arcan.secret', 'DATABASE_URL', realm=app_realm, env=app_env) }}"
redis_url: "{{ lookup('getarcan.arcan.secret', 'REDIS_URL', realm=app_realm, env=app_env) }}"
aws_key: "{{ lookup('getarcan.arcan.secret', 'AWS_ACCESS_KEY_ID', realm=app_realm, env=app_env) }}"
aws_secret: "{{ lookup('getarcan.arcan.secret', 'AWS_SECRET_ACCESS_KEY', realm=app_realm, env=app_env) }}"
no_log: true

- name: Render worker config
template:
src: templates/worker-config.toml.j2
dest: /opt/myapp/worker-config.toml
owner: myapp
group: myapp
mode: "0600"
notify: Restart worker

handlers:
- name: Restart worker
systemd:
name: myapp-worker
state: restarted

The Jinja2 config template used by the API servers:

{# templates/api-config.toml.j2 #}
# Application configuration — managed by Ansible + Arcan
# Deployed: {{ ansible_date_time.iso8601 }}
# Host: {{ inventory_hostname }}
# DO NOT EDIT MANUALLY — changes will be overwritten on next deploy

[database]
url = "{{ db_url }}"
pool_size = 25
idle_timeout = "5m"

[cache]
url = "{{ redis_url }}"
prefix = "myapp:"

[payments]
stripe_secret_key = "{{ stripe_key }}"
webhook_signing_secret = "{{ lookup('getarcan.arcan.secret', 'STRIPE_WEBHOOK_SECRET', realm=app_realm, env=app_env) }}"

[monitoring]
sentry_dsn = "{{ sentry_dsn }}"
environment = "{{ app_env }}"
server_name = "{{ inventory_hostname }}"

[auth]
jwt_secret = "{{ jwt_secret }}"
jwt_issuer = "https://myapp.com"
jwt_ttl = "24h"

[server]
host = "0.0.0.0"
port = 8080
debug = false

And the worker config template:

{# templates/worker-config.toml.j2 #}
# Worker configuration — managed by Ansible + Arcan
# Deployed: {{ ansible_date_time.iso8601 }}

[database]
url = "{{ db_url }}"
pool_size = 10

[cache]
url = "{{ redis_url }}"

[aws]
access_key_id = "{{ aws_key }}"
secret_access_key = "{{ aws_secret }}"
region = "us-east-1"

[worker]
concurrency = {{ ansible_processor_vcpus | default(4) }}
queues = ["default", "emails", "reports"]
tip

Always use no_log: true on tasks that handle secret values to prevent them from appearing in Ansible output. The module automatically suppresses the value field in its output, but set_fact and debug tasks do not.

Requirements

  • Python >= 3.8
  • Ansible >= 2.9