Fix P1 WEBRUNNER deployment failures and secret leakage

- providers/webrunner.yml: wrap loop_var under loop_control in all three
  provider provision tasks — Ansible was rejecting loop_var as a conflicting
  action statement alongside include_tasks
- deployment_engine.py: strip _SECRET_KEYS (tokens, passwords, api keys)
  from create_extra_vars() — credentials are already set as env vars by
  set_provider_environment(), no need to pass them through --extra-vars
  where they end up in process listings and logs
- deployment_engine.py: remove debug log line that dumped the full
  ansible-playbook command including all extra-vars JSON with live creds
- deployment_engine.py: create deployment log files with 0o600 permissions
  instead of world-readable 0o644
- deploy_webrunner.py: remove load_vars_file() call that was pulling
  phishing module creds (SMTP password, gophish port, domain) into the
  webrunner config dict — all provider creds already collected by
  gather_provider_config()
- deploy_webrunner.py: remove unused select_provider import
- .gitignore: narrow 'deployment*' glob to specific artifact patterns so
  deployment_engine.py is no longer excluded from tracking
This commit is contained in:
n0mad1k
2026-04-30 19:04:42 -04:00
parent d2b2bfb591
commit 6538b09b7d
4 changed files with 348 additions and 12 deletions
+4 -1
View File
@@ -1,6 +1,9 @@
vars.yaml
venv
deployment*
deployment_*.log
deployment_info_*.txt
node_chunks_*.json
scanner_ips_*.txt
config.yml
logs/
domainhunter/
+1 -8
View File
@@ -17,9 +17,8 @@ if _base not in sys.path:
from utils.common import (
COLORS, clear_screen, print_banner, generate_deployment_id,
setup_logging, get_public_ip, confirm_action, wait_for_input,
load_vars_file,
)
from utils.provider_utils import select_provider, gather_provider_config
from utils.provider_utils import gather_provider_config
from utils.ssh_utils import generate_ssh_key
from utils.naming_utils import show_naming_relationship
from utils.deployment_engine import execute_playbook, set_provider_environment
@@ -353,12 +352,6 @@ def gather_webrunner_parameters() -> dict | None:
else:
print(f"{COLORS['CYAN']} Nodes will remain running after scan — remember to teardown manually.{COLORS['RESET']}")
# Merge any provider vars not already set
for provider in providers:
vars_data = load_vars_file(provider)
if vars_data:
config.update({k: v for k, v in vars_data.items() if k not in config})
# Apply per-provider instance defaults if not set by gather_provider_config
if 'linode_instance_type' not in config:
config['linode_instance_type'] = DEFAULT_INSTANCE.get('linode', 'g6-standard-2')
+6 -3
View File
@@ -29,17 +29,20 @@
- name: Provision Linode nodes
include_tasks: "Linode/webrunner_provision_tasks.yml"
loop: "{{ all_node_chunks | selectattr('provider', 'equalto', 'linode') | list }}"
loop_var: node_chunk
loop_control:
loop_var: node_chunk
- name: Provision AWS nodes
include_tasks: "AWS/webrunner_provision_tasks.yml"
loop: "{{ all_node_chunks | selectattr('provider', 'equalto', 'aws') | list }}"
loop_var: node_chunk
loop_control:
loop_var: node_chunk
- name: Provision FlokiNET nodes
include_tasks: "FlokiNET/webrunner_provision_tasks.yml"
loop: "{{ all_node_chunks | selectattr('provider', 'equalto', 'flokinet') | list }}"
loop_var: node_chunk
loop_control:
loop_var: node_chunk
- name: WEBRUNNER — Configure and scan
+337
View File
@@ -0,0 +1,337 @@
#!/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:
# Try to load from vars file
try:
from utils.common import load_vars_file
vars_data = load_vars_file(provider)
if vars_data and vars_data.get('linode_token'):
os.environ['LINODE_TOKEN'] = vars_data['linode_token']
config['linode_token'] = vars_data['linode_token']
except Exception as e:
logging.warning(f"Failed to load Linode token from vars file: {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
"""
try:
# Build the ansible-playbook command
cmd = [
'ansible-playbook',
playbook,
'--extra-vars', create_extra_vars(config)
]
# 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
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
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")