Files
CoM-c2itall/utils/deployment_engine.py
T
n0mad1k 8e6b323fb2 Route Ansible secrets via temp file instead of extra-vars JSON
Passes linode_token and other secrets to Ansible through a 0o600 temp
file (@tmpfile) rather than the main --extra-vars JSON blob. Fixes
undefined variable errors for provision tasks that reference linode_token
as an Ansible variable, while keeping secrets out of process listings.
Temp file is removed in finally block regardless of outcome.

Also fixes absolute path for node_chunks_file and other logs-relative
paths so Ansible lookup('file', ...) resolves correctly from any
playbook directory.
2026-04-30 21:11:05 -04:00

354 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Deployment engine for C2ingRed infrastructure deployments
"""
import os
import sys
import subprocess
import logging
import json
# Add project root to path
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from utils.common import COLORS, PROVIDER_DIRS
_SECRET_KEYS = frozenset({
'linode_token', 'aws_access_key', 'aws_secret_key', 'flokinet_api_key',
'smtp_auth_pass', 'smtp_password', 'ftp_password', 'api_key',
'password', 'secret', 'token',
})
def deploy_infrastructure(config):
"""
Deploy infrastructure based on the provided configuration.
Args:
config: Dictionary containing deployment configuration
Returns:
bool: True if deployment succeeded, False otherwise
"""
provider = config.get('provider')
deployment_id = config.get('deployment_id')
if not provider or not deployment_id:
logging.error("Missing provider or deployment ID in configuration")
return False
# Determine which playbook to use
playbook = determine_playbook(config)
if not playbook:
logging.error("Could not determine appropriate playbook for deployment")
return False
if not os.path.exists(playbook):
logging.error(f"Playbook not found: {playbook}")
print(f"{COLORS['RED']}Playbook not found: {playbook}{COLORS['RESET']}")
return False
logging.info(f"Using playbook: {playbook}")
# Set provider-specific environment variables
set_provider_environment(config)
# Execute the deployment
return execute_playbook(playbook, config)
def determine_playbook(config):
"""
Determine the correct playbook based on deployment configuration.
Args:
config: Deployment configuration dictionary
Returns:
str: Path to the playbook file
"""
provider = config.get('provider')
provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize())
base_path = f"providers/{provider_dir}"
# Attack box deployment
if config.get('attack_box_deployment'):
return f"{base_path}/attack_box.yml"
# Redirector-only deployment
if config.get('redirector_only'):
return f"{base_path}/redirector.yml"
# Tracker deployment
if config.get('tracker_deployment'):
return f"{base_path}/tracker.yml"
# Phishing infrastructure deployment
if config.get('phishing_deployment'):
# Check for provider-specific phishing playbook first
provider_specific = f"{base_path}/{provider}_phishing.yml"
if os.path.exists(provider_specific):
return provider_specific
return f"{base_path}/phishing.yml"
# Chat server deployment
if config.get('chat_deployment'):
return f"{base_path}/chat_server.yml"
# C2 deployment (default for C2 infrastructure)
if config.get('c2_only') or config.get('deploy_c2') or config.get('c2_framework'):
return f"{base_path}/c2.yml"
# Default to C2 playbook
return f"{base_path}/c2.yml"
def set_provider_environment(config):
"""
Set provider-specific environment variables for deployment.
Args:
config: Deployment configuration dictionary
"""
provider = config.get('provider')
if provider == "aws":
if config.get('aws_access_key'):
os.environ['AWS_ACCESS_KEY_ID'] = config['aws_access_key']
if config.get('aws_secret_key'):
os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret_key']
if config.get('aws_region'):
os.environ['AWS_DEFAULT_REGION'] = config['aws_region']
elif provider == "linode":
if config.get('linode_token'):
os.environ['LINODE_TOKEN'] = config['linode_token']
else:
# Pull from Infisical at runtime
try:
token = subprocess.check_output(
[os.path.expanduser('~/.local/bin/creds'), 'get', 'LINODE_TOKEN', 'homelab'],
text=True, stderr=subprocess.DEVNULL,
).strip()
if token:
os.environ['LINODE_TOKEN'] = token
config['linode_token'] = token
except Exception as e:
logging.warning(f"Failed to load Linode token from Infisical: {e}")
elif provider == "flokinet":
if config.get('flokinet_api_key'):
os.environ['FLOKINET_API_KEY'] = config['flokinet_api_key']
def execute_playbook(playbook, config):
"""
Execute an Ansible playbook with the provided configuration.
Args:
playbook: Path to the Ansible playbook
config: Deployment configuration dictionary
Returns:
bool: True if playbook executed successfully, False otherwise
"""
import tempfile
secret_vars_file = None
try:
# Build the ansible-playbook command
cmd = [
'ansible-playbook',
playbook,
'--extra-vars', create_extra_vars(config)
]
# Write sensitive vars to a temp file (0o600) so they reach Ansible as
# variables without appearing in process listings or the main extra-vars JSON.
secret_vars = {k: v for k, v in config.items()
if v is not None
and any(s in k.lower() for s in _SECRET_KEYS)}
if secret_vars:
fd, secret_vars_file = tempfile.mkstemp(suffix='.json', prefix='ansible_secret_')
os.chmod(secret_vars_file, 0o600)
with os.fdopen(fd, 'w') as f:
json.dump(secret_vars, f)
cmd += ['--extra-vars', f'@{secret_vars_file}']
# Add verbosity if debug mode is enabled
if config.get('debug'):
cmd.append('-vvv')
logging.info(f"Executing: {' '.join(cmd[:3])} ...")
# Get log file path for output
log_file = f"logs/deployment_{config['deployment_id']}.log"
# Execute the playbook — log file restricted to owner-only (contains Ansible output)
log_fd = os.open(log_file, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
with open(log_fd, 'a') as log_output:
# Write command to log
log_output.write(f"\n{'='*60}\n")
log_output.write(f"Executing playbook: {playbook}\n")
log_output.write(f"{'='*60}\n\n")
log_output.flush()
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
# Stream output to both console and log file
for line in process.stdout:
# Write to log file
log_output.write(line)
log_output.flush()
# Print select lines to console for user feedback
if should_print_line(line):
print(line.rstrip())
process.wait()
if process.returncode == 0:
logging.info("Playbook executed successfully")
save_deployment_info(config)
return True
else:
logging.error(f"Playbook failed with return code {process.returncode}")
print(f"\n{COLORS['RED']}Deployment failed. Check logs for details: {log_file}{COLORS['RESET']}")
return False
except FileNotFoundError:
logging.error("ansible-playbook command not found. Is Ansible installed?")
print(f"{COLORS['RED']}Error: ansible-playbook not found. Please install Ansible.{COLORS['RESET']}")
return False
except Exception as e:
logging.error(f"Error executing playbook: {e}")
print(f"{COLORS['RED']}Error executing playbook: {e}{COLORS['RESET']}")
return False
finally:
if secret_vars_file and os.path.exists(secret_vars_file):
os.unlink(secret_vars_file)
def should_print_line(line):
"""
Determine if a line should be printed to console.
Filters out verbose Ansible output to show only key information.
Args:
line: Output line from Ansible
Returns:
bool: True if line should be printed
"""
# Always show task names and important messages
important_patterns = [
'TASK [',
'PLAY [',
'ok:',
'changed:',
'failed:',
'fatal:',
'skipping:',
'PLAY RECAP',
'=> {',
'msg:',
'IP Address:',
'Instance ID:',
]
for pattern in important_patterns:
if pattern in line:
return True
return False
def create_extra_vars(config):
"""
Create JSON string of extra variables for Ansible.
Args:
config: Deployment configuration dictionary
Returns:
str: JSON string of variables
"""
# Credentials are passed via environment variables (set_provider_environment).
# Strip them here so they never appear in --extra-vars or process listings.
extra_vars = {}
for key, value in config.items():
if value is None:
continue
if any(secret in key.lower() for secret in _SECRET_KEYS):
continue
if isinstance(value, bool):
extra_vars[key] = value
elif isinstance(value, (str, int, float, list, dict)):
extra_vars[key] = value
return json.dumps(extra_vars)
def save_deployment_info(config):
"""
Save deployment information to a file for later reference/cleanup.
Args:
config: Deployment configuration dictionary
"""
try:
deployment_id = config.get('deployment_id')
info_file = f"logs/deployment_info_{deployment_id}.txt"
with open(info_file, 'w') as f:
f.write(f"Deployment Information\n")
f.write(f"{'='*50}\n")
f.write(f"Deployment ID: {deployment_id}\n")
f.write(f"Provider: {config.get('provider')}\n")
f.write(f"Deployment Type: {config.get('deployment_type', 'unknown')}\n")
f.write(f"\n")
f.write(f"Configuration:\n")
f.write(f"{'-'*30}\n")
# Write key configuration items
keys_to_save = [
'provider', 'deployment_id', 'deployment_type',
'attack_box_name', 'c2_name', 'redirector_name', 'tracker_name',
'chat_server_name',
'domain', 'c2_subdomain', 'redirector_subdomain',
'c2_framework', 'redirector_type',
'linode_region', 'aws_region',
'setup_vpn', 'setup_tor',
'ssh_key_path',
'matrix_admin_user', 'enable_bot_support'
]
for key in keys_to_save:
if key in config and config[key] is not None:
# Don't save sensitive data
if 'password' in key.lower() or 'token' in key.lower() or 'secret' in key.lower():
continue
f.write(f"{key}: {config[key]}\n")
f.write(f"\n")
f.write(f"Access Information:\n")
f.write(f"{'-'*30}\n")
f.write(f"(Instance IPs will be displayed in Ansible output above)\n")
logging.info(f"Deployment info saved to: {info_file}")
except Exception as e:
logging.warning(f"Failed to save deployment info: {e}")
if __name__ == "__main__":
print("Deployment engine loaded")