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"]
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