Skip to content

Instantly share code, notes, and snippets.

@austinsonger
Last active October 5, 2025 00:21
Show Gist options
  • Save austinsonger/a1593979312c695b4007edc53e95cea1 to your computer and use it in GitHub Desktop.
Save austinsonger/a1593979312c695b4007edc53e95cea1 to your computer and use it in GitHub Desktop.
phase
#!/bin/bash
# Modified: 2025-10-05
# Phase Console Installation Script for Rocky Linux and CentOS
# Installs and configures Phase Console server with Tor hidden service
set -euo pipefail
# Configuration variables
PHASE_CONSOLE_DIR="/opt/phase-console"
PHASE_USER="phaseuser"
PHASE_GROUP="phaseuser"
TOR_DATA_DIR="/var/lib/tor"
SSH_PORT="2222"
PHASE_CONSOLE_PORT="3000"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging function
log() {
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
}
warn() {
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING:${NC} $1"
}
error() {
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR:${NC} $1"
exit 1
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root"
fi
}
# Check OS version (Rocky Linux or CentOS)
check_os_version() {
if ! grep -qE "(Rocky Linux|CentOS)" /etc/os-release; then
error "This script is designed for Rocky Linux or CentOS"
fi
local os_name=$(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)
local version=$(grep VERSION_ID /etc/os-release | cut -d'"' -f2)
log "Detected OS: $os_name (version: $version)"
}
# Update system packages
update_system() {
log "Updating system packages..."
dnf update -y
dnf install -y epel-release
dnf update -y
}
# Install required packages
install_packages() {
log "Installing required packages..."
# Core packages
dnf install -y \
curl \
wget \
git \
jq \
openssl \
firewalld \
policycoreutils-python-utils \
selinux-policy-devel
# Install Docker
log "Installing Docker..."
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Install Tor
log "Installing Tor..."
dnf install -y tor
# Install OpenSSH server
log "Installing OpenSSH server..."
dnf install -y openssh-server
}
# Ensure Tor user and group exist
ensure_tor_user() {
log "Ensuring Tor user and group exist..."
# Check if tor group exists, create if not
if ! getent group tor >/dev/null 2>&1; then
log "Creating tor group..."
groupadd -r tor
else
log "Tor group already exists"
fi
# Check if tor user exists, create if not
if ! getent passwd tor >/dev/null 2>&1; then
log "Creating tor user..."
useradd -r -g tor -d /var/lib/tor -s /bin/false -c "Tor daemon user" tor
else
log "Tor user already exists"
fi
# Ensure tor user home directory exists with correct ownership
if [[ ! -d /var/lib/tor ]]; then
mkdir -p /var/lib/tor
fi
chown tor:tor /var/lib/tor
chmod 700 /var/lib/tor
log "Tor user and group setup completed"
}
# Configure system services
configure_services() {
log "Configuring system services..."
# Enable and start Docker
systemctl enable docker
systemctl start docker
# Enable and start SSH
systemctl enable sshd
systemctl start sshd
# Configure Tor (will start later after configuration)
systemctl enable tor
}
# Create Phase Console user
create_phase_user() {
log "Creating Phase Console user..."
if ! id "$PHASE_USER" &>/dev/null; then
useradd -r -m -s /bin/bash -d "/home/$PHASE_USER" "$PHASE_USER"
usermod -aG docker "$PHASE_USER"
else
log "User $PHASE_USER already exists"
fi
}
# Create directory structure
create_directories() {
log "Creating directory structure..."
mkdir -p "$PHASE_CONSOLE_DIR"/{data,config,logs,ssh-keys,secrets}
mkdir -p "$TOR_DATA_DIR/phase-console"
mkdir -p "/home/$PHASE_USER/.ssh"
# Set ownership
chown -R "$PHASE_USER:$PHASE_GROUP" "$PHASE_CONSOLE_DIR"
chown -R "$PHASE_USER:$PHASE_GROUP" "/home/$PHASE_USER"
# Verify tor user exists before setting ownership
if getent passwd tor >/dev/null 2>&1; then
chown -R tor:tor "$TOR_DATA_DIR"
log "Set Tor data directory ownership to tor:tor"
else
error "Tor user does not exist. This should have been created by ensure_tor_user function."
fi
# Set permissions
chmod 700 "/home/$PHASE_USER/.ssh"
chmod 700 "$PHASE_CONSOLE_DIR/secrets"
chmod 755 "$PHASE_CONSOLE_DIR"/{data,config,logs,ssh-keys}
}
# Generate secrets
generate_secrets() {
log "Generating secrets..."
local secrets_dir="$PHASE_CONSOLE_DIR/secrets"
# Phase Console infrastructure secrets
log " Generating Phase Console infrastructure secrets..."
openssl rand -base64 32 > "$secrets_dir/postgres_password"
openssl rand -base64 32 > "$secrets_dir/redis_password"
openssl rand -base64 64 > "$secrets_dir/jwt_secret"
openssl rand -base64 32 > "$secrets_dir/encryption_key"
# ShadowBin Marketplace application secrets
log " Generating ShadowBin Marketplace application secrets..."
echo "base64:$(openssl rand -base64 32)" > "$secrets_dir/app_key"
openssl rand -base64 32 > "$secrets_dir/session_encryption_key"
openssl rand -hex 32 > "$secrets_dir/sanctum_secret_key"
openssl rand -base64 64 > "$secrets_dir/jwt_secret_app"
openssl rand -base64 32 > "$secrets_dir/encryption_master_key"
# Database secrets for ShadowBin
log " Generating database secrets..."
openssl rand -base64 24 > "$secrets_dir/db_password"
openssl rand -base64 32 > "$secrets_dir/db_root_password"
openssl rand -base64 24 > "$secrets_dir/redis_app_password"
# External API key placeholders (to be updated with real keys)
log " Creating external API key placeholders..."
echo "your_coinmarketcap_api_key_here" > "$secrets_dir/coinmarketcap_api_key"
echo "your_coingecko_api_key_here" > "$secrets_dir/coingecko_api_key"
echo "your_kraken_api_key_here" > "$secrets_dir/kraken_api_key"
echo "your_kraken_api_secret_here" > "$secrets_dir/kraken_api_secret"
# Monitoring secrets
log " Generating monitoring secrets..."
openssl rand -hex 32 > "$secrets_dir/telescope_secret"
openssl rand -hex 16 > "$secrets_dir/health_check_secret"
openssl rand -hex 24 > "$secrets_dir/metrics_api_token"
openssl rand -base64 32 > "$secrets_dir/log_encryption_key"
# Backup and recovery secrets
log " Generating backup secrets..."
openssl rand -base64 32 > "$secrets_dir/backup_encryption_key"
echo "your_s3_access_key_here" > "$secrets_dir/s3_access_key_id"
echo "your_s3_secret_key_here" > "$secrets_dir/s3_secret_access_key"
openssl rand -base64 32 > "$secrets_dir/disaster_recovery_key"
# Tor and network security
log " Generating network security secrets..."
openssl rand -base64 24 > "$secrets_dir/tor_control_password"
openssl rand -base64 32 > "$secrets_dir/proxy_auth_token"
# Set secure permissions
chmod 600 "$secrets_dir"/*
chown "$PHASE_USER:$PHASE_GROUP" "$secrets_dir"/*
log " Generated $(ls -1 "$secrets_dir" | wc -l) secrets for ShadowBin Marketplace"
}
# Configure Tor
configure_tor() {
log "Configuring Tor hidden service..."
# Backup original torrc
cp /etc/tor/torrc /etc/tor/torrc.backup
# Create Tor configuration
cat > /etc/tor/torrc << 'EOF'
# Phase Console Tor Configuration
# Created: 2025-09-14
# Basic configuration
User tor
DataDirectory /var/lib/tor
PidFile /var/run/tor/tor.pid
# Logging
Log notice file /var/log/tor/tor.log
Log warn file /var/log/tor/warn.log
# Hidden Service for Phase Console
HiddenServiceDir /var/lib/tor/phase-console/
HiddenServicePort 80 127.0.0.1:3000
HiddenServicePort 22 127.0.0.1:2222
# Security hardening
HiddenServiceVersion 3
HiddenServiceNumIntroductionPoints 10
HiddenServiceMaxStreams 65536
HiddenServiceMaxStreamsCloseCircuit 1
# Disable client functionality
SocksPort 0
ControlPort 0
CookieAuthentication 0
# Additional security
AvoidDiskWrites 1
DisableDebuggerAttachment 1
SafeLogging 1
WarnUnsafeSocks 1
# Circuit security
CircuitBuildTimeout 30
LearnCircuitBuildTimeout 0
MaxCircuitDirtiness 300
NewCircuitPeriod 15
MaxClientCircuitsPending 32
EOF
# Create log directory
mkdir -p /var/log/tor
chown tor:tor /var/log/tor
chmod 755 /var/log/tor
# Set SELinux context if enabled
if command -v setsebool &> /dev/null; then
setsebool -P tor_can_network_relay 1
setsebool -P tor_bind_all_unreserved_ports 1
fi
}
# Configure SSH
configure_ssh() {
log "Configuring SSH server..."
# Backup original sshd_config
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
# Create SSH configuration for Phase Console
cat >> /etc/ssh/sshd_config << EOF
# Phase Console SSH Configuration
# Added: 2025-09-14
Port $SSH_PORT
Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
# Security hardening
MaxAuthTries 3
MaxSessions 2
ClientAliveInterval 300
ClientAliveCountMax 2
TCPKeepAlive no
Compression no
# Restrict user access
AllowUsers $PHASE_USER
DenyUsers root
# Logging
LogLevel VERBOSE
SyslogFacility AUTH
# Disable unnecessary features
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
PermitTunnel no
GatewayPorts no
EOF
# Generate SSH host keys for custom port if needed
if [[ ! -f "/etc/ssh/ssh_host_rsa_key" ]]; then
ssh-keygen -A
fi
}
# Configure firewall
configure_firewall() {
log "Configuring firewall..."
# Start and enable firewalld
systemctl enable firewalld
systemctl start firewalld
# Allow SSH on custom port
firewall-cmd --permanent --add-port="$SSH_PORT/tcp"
# Allow Phase Console port (only from localhost)
firewall-cmd --permanent --add-rich-rule="rule family='ipv4' source address='127.0.0.1' port protocol='tcp' port='$PHASE_CONSOLE_PORT' accept"
# Allow Tor
firewall-cmd --permanent --add-service=tor
# Reload firewall
firewall-cmd --reload
log "Firewall configured. Open ports: $SSH_PORT/tcp (SSH), $PHASE_CONSOLE_PORT/tcp (localhost only)"
}
# Create Docker Compose file
create_docker_compose() {
log "Creating Docker Compose configuration..."
cat > "$PHASE_CONSOLE_DIR/docker-compose.yml" << 'EOF'
# Phase Console Docker Compose
# Created: 2025-09-14
version: '3.8'
name: phase-console-server
networks:
phase-internal:
driver: bridge
ipam:
config:
- subnet: 172.30.0.0/16
volumes:
phase-postgres-data:
name: phase-postgres-data
phase-redis-data:
name: phase-redis-data
phase-app-data:
name: phase-app-data
services:
postgres:
image: postgres:15-alpine
container_name: phase-postgres
restart: unless-stopped
environment:
POSTGRES_DB: phase_console
POSTGRES_USER: phase_user
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
secrets:
- postgres_password
volumes:
- phase-postgres-data:/var/lib/postgresql/data
networks:
- phase-internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U phase_user -d phase_console"]
interval: 30s
timeout: 10s
retries: 3
redis:
image: redis:7-alpine
container_name: phase-redis
restart: unless-stopped
command: ["redis-server", "--requirepass", "$(cat /run/secrets/redis_password)"]
secrets:
- redis_password
volumes:
- phase-redis-data:/data
networks:
- phase-internal
healthcheck:
test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "$(cat /run/secrets/redis_password)", "ping"]
interval: 30s
timeout: 10s
retries: 3
phase-console:
image: phasehq/console:latest
container_name: phase-console-app
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
- DATABASE_URL=postgresql://phase_user:$(cat /run/secrets/postgres_password)@postgres:5432/phase_console
- REDIS_URL=redis://:$(cat /run/secrets/redis_password)@redis:6379
- JWT_SECRET_FILE=/run/secrets/jwt_secret
- ENCRYPTION_KEY_FILE=/run/secrets/encryption_key
- NODE_ENV=production
secrets:
- postgres_password
- redis_password
- jwt_secret
- encryption_key
volumes:
- phase-app-data:/app/data
networks:
- phase-internal
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
secrets:
postgres_password:
file: ./secrets/postgres_password
redis_password:
file: ./secrets/redis_password
jwt_secret:
file: ./secrets/jwt_secret
encryption_key:
file: ./secrets/encryption_key
EOF
chown "$PHASE_USER:$PHASE_GROUP" "$PHASE_CONSOLE_DIR/docker-compose.yml"
}
# Create systemd service for Phase Console
create_systemd_service() {
log "Creating systemd service for Phase Console..."
cat > /etc/systemd/system/phase-console.service << EOF
[Unit]
Description=Phase Console Server
Requires=docker.service tor.service
After=docker.service tor.service
StartLimitIntervalSec=0
[Service]
Type=oneshot
RemainAfterExit=yes
User=$PHASE_USER
Group=$PHASE_GROUP
WorkingDirectory=$PHASE_CONSOLE_DIR
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
ExecReload=/usr/bin/docker compose restart
TimeoutStartSec=300
TimeoutStopSec=120
Restart=on-failure
RestartSec=30
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable phase-console.service
}
# Create management scripts
create_management_scripts() {
log "Creating management scripts..."
# Phase Console control script
cat > "$PHASE_CONSOLE_DIR/phase-control.sh" << 'EOF'
#!/bin/bash
# Phase Console Control Script
# Created: 2025-09-14
set -euo pipefail
PHASE_CONSOLE_DIR="/opt/phase-console"
cd "$PHASE_CONSOLE_DIR"
case "${1:-}" in
start)
echo "Starting Phase Console..."
docker compose up -d
;;
stop)
echo "Stopping Phase Console..."
docker compose down
;;
restart)
echo "Restarting Phase Console..."
docker compose restart
;;
status)
echo "Phase Console Status:"
docker compose ps
;;
logs)
docker compose logs -f "${2:-}"
;;
update)
echo "Updating Phase Console..."
docker compose pull
docker compose up -d
;;
backup)
echo "Creating backup..."
backup_dir="/opt/phase-console/backups/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$backup_dir"
docker compose exec postgres pg_dump -U phase_user phase_console > "$backup_dir/database.sql"
cp -r ./secrets "$backup_dir/"
echo "Backup created at: $backup_dir"
;;
*)
echo "Usage: $0 {start|stop|restart|status|logs|update|backup}"
echo " logs [service] - Show logs for specific service or all"
exit 1
;;
esac
EOF
chmod +x "$PHASE_CONSOLE_DIR/phase-control.sh"
chown "$PHASE_USER:$PHASE_GROUP" "$PHASE_CONSOLE_DIR/phase-control.sh"
# Create symlink for easy access
ln -sf "$PHASE_CONSOLE_DIR/phase-control.sh" /usr/local/bin/phase-console
# SSH key management script
cat > "$PHASE_CONSOLE_DIR/manage-ssh-keys.sh" << 'EOF'
#!/bin/bash
# SSH Key Management Script for Phase Console
# Created: 2025-09-14
set -euo pipefail
PHASE_USER="phaseuser"
SSH_DIR="/home/$PHASE_USER/.ssh"
AUTHORIZED_KEYS="$SSH_DIR/authorized_keys"
case "${1:-}" in
add)
if [[ -z "${2:-}" ]]; then
echo "Usage: $0 add <public_key_file_or_string>"
exit 1
fi
if [[ -f "$2" ]]; then
# It's a file
cat "$2" >> "$AUTHORIZED_KEYS"
echo "Added public key from file: $2"
else
# It's a key string
echo "$2" >> "$AUTHORIZED_KEYS"
echo "Added public key string"
fi
# Set proper permissions
chmod 600 "$AUTHORIZED_KEYS"
chown "$PHASE_USER:$PHASE_USER" "$AUTHORIZED_KEYS"
;;
list)
echo "Authorized SSH keys:"
if [[ -f "$AUTHORIZED_KEYS" ]]; then
cat -n "$AUTHORIZED_KEYS"
else
echo "No authorized keys found"
fi
;;
remove)
if [[ -z "${2:-}" ]]; then
echo "Usage: $0 remove <line_number>"
exit 1
fi
if [[ -f "$AUTHORIZED_KEYS" ]]; then
sed -i "${2}d" "$AUTHORIZED_KEYS"
echo "Removed key at line $2"
else
echo "No authorized keys file found"
fi
;;
clear)
read -p "Are you sure you want to remove all SSH keys? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
> "$AUTHORIZED_KEYS"
echo "All SSH keys removed"
fi
;;
*)
echo "Usage: $0 {add|list|remove|clear}"
echo " add <key_file_or_string> - Add SSH public key"
echo " list - List all authorized keys"
echo " remove <line_number> - Remove key at specific line"
echo " clear - Remove all keys"
exit 1
;;
esac
EOF
chmod +x "$PHASE_CONSOLE_DIR/manage-ssh-keys.sh"
chown "$PHASE_USER:$PHASE_GROUP" "$PHASE_CONSOLE_DIR/manage-ssh-keys.sh"
# Create symlink for easy access
ln -sf "$PHASE_CONSOLE_DIR/manage-ssh-keys.sh" /usr/local/bin/phase-ssh-keys
}
# Start services
start_services() {
log "Starting services..."
# Start Tor first
systemctl start tor
sleep 5
# Start SSH
systemctl restart sshd
# Start Phase Console
systemctl start phase-console
# Wait for services to be ready
log "Waiting for services to start..."
sleep 30
}
# Get Tor onion address
get_onion_address() {
log "Retrieving Tor onion address..."
local onion_file="$TOR_DATA_DIR/phase-console/hostname"
local max_attempts=30
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
if [[ -f "$onion_file" ]]; then
local onion_address=$(cat "$onion_file")
log "Phase Console .onion address: $onion_address"
# Save to easily accessible location
echo "$onion_address" > "$PHASE_CONSOLE_DIR/onion-address.txt"
chown "$PHASE_USER:$PHASE_GROUP" "$PHASE_CONSOLE_DIR/onion-address.txt"
return 0
fi
log "Waiting for Tor to generate onion address... (attempt $attempt/$max_attempts)"
sleep 2
((attempt++))
done
warn "Could not retrieve onion address. Check Tor logs: journalctl -u tor"
}
# Create installation summary
create_summary() {
log "Creating installation summary..."
local summary_file="$PHASE_CONSOLE_DIR/installation-summary.txt"
local onion_address="Unknown"
if [[ -f "$PHASE_CONSOLE_DIR/onion-address.txt" ]]; then
onion_address=$(cat "$PHASE_CONSOLE_DIR/onion-address.txt")
fi
cat > "$summary_file" << EOF
Phase Console Installation Summary
==================================
Installation Date: $(date)
Server: $(hostname)
OS: $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)
Services:
---------
Phase Console: http://127.0.0.1:$PHASE_CONSOLE_PORT
Tor Hidden Service: http://$onion_address
SSH Port: $SSH_PORT
Directories:
------------
Installation: $PHASE_CONSOLE_DIR
Secrets: $PHASE_CONSOLE_DIR/secrets
Logs: $PHASE_CONSOLE_DIR/logs
Tor Data: $TOR_DATA_DIR/phase-console
Management Commands:
-------------------
Control Phase Console: phase-console {start|stop|restart|status|logs|update|backup}
Manage SSH Keys: phase-ssh-keys {add|list|remove|clear}
Generated Secrets for ShadowBin Marketplace:
--------------------------------------------
Application Secrets:
- app_key (Laravel encryption key)
- session_encryption_key (Session encryption)
- sanctum_secret_key (API authentication)
- jwt_secret_app (JWT signing)
- encryption_master_key (Master encryption)
Database Secrets:
- db_password (MySQL application user)
- db_root_password (MySQL root user)
- redis_app_password (Redis authentication)
Monitoring Secrets:
- telescope_secret (Laravel Telescope)
- health_check_secret (Health endpoints)
- metrics_api_token (Metrics collection)
- log_encryption_key (Log encryption)
Backup & Recovery:
- backup_encryption_key (Backup encryption)
- disaster_recovery_key (Emergency access)
External API Placeholders (UPDATE WITH REAL VALUES):
- coinmarketcap_api_key
- coingecko_api_key
- kraken_api_key
- kraken_api_secret
- s3_access_key_id
- s3_secret_access_key
Service Status:
---------------
$(systemctl is-active docker) - Docker
$(systemctl is-active tor) - Tor
$(systemctl is-active sshd) - SSH
$(systemctl is-active phase-console) - Phase Console
Next Steps:
-----------
1. Add SSH public keys: phase-ssh-keys add <public_key_file>
2. UPDATE PLACEHOLDER API KEYS with real values in $PHASE_CONSOLE_DIR/secrets/
3. Configure ShadowBin Marketplace to connect to: $onion_address
4. Test SSH connection: ssh -p $SSH_PORT phaseuser@$onion_address
5. Access Phase Console: http://127.0.0.1:$PHASE_CONSOLE_PORT (local only)
6. Run post-install validation: phase-console-post-install.sh
Security Notes:
---------------
- Phase Console is only accessible via Tor hidden service
- SSH access requires public key authentication
- All secrets are stored in $PHASE_CONSOLE_DIR/secrets with restricted permissions
- Firewall is configured to allow only necessary ports
- Generated $(ls -1 $PHASE_CONSOLE_DIR/secrets | wc -l) secrets for ShadowBin Marketplace
EOF
chown "$PHASE_USER:$PHASE_GROUP" "$summary_file"
# Display summary
cat "$summary_file"
}
# Main installation function
main() {
log "Starting Phase Console installation on Rocky Linux/CentOS..."
check_root
check_os_version
update_system
install_packages
ensure_tor_user
configure_services
create_phase_user
create_directories
generate_secrets
configure_tor
configure_ssh
configure_firewall
create_docker_compose
create_systemd_service
create_management_scripts
start_services
get_onion_address
create_summary
log "Phase Console installation completed successfully!"
log "Summary saved to: $PHASE_CONSOLE_DIR/installation-summary.txt"
log "Onion address saved to: $PHASE_CONSOLE_DIR/onion-address.txt"
}
# Run main function if script is executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment