Operations

VPN & Internal Tools

Production AWS resources — Aurora, Redis, ClickHouse, Bull Studio, and RedisInsight — all live in private VPC subnets with no public internet access. WireGuard VPN tunnels your traffic through the Zelly bastion EC2 into the production VPC. Once connected, every private resource is reachable by its VPC private IP — no per-port SSH tunnels needed for Redis, Aurora, or ClickHouse.

Staging Aurora is publicly accessible so VPN is not required for staging database access. For everything else in production, connect first.

i
If you've installed WireGuard before and have a working conf file, skip to Step 3 — Connect.

What lives behind the VPN

ResourceHow to find the addressPortNotes
Aurora MySQLSecrets Manager: zelly/aurora/master → .host3306Prod only — staging is public
ElastiCache RedisSecrets Manager: zelly/redis/auth → .host6379 TLSBoth envs; TLS required
ClickHouseSecrets Manager: zelly/clickhouse/env → .host8123 HTTP · 9000 nativeEC2 in private subnet
Bull StudioECS task private IP — see below3000Direct browser access via VPN
RedisInsightECS task private IP — see below5540Direct browser access via VPN
Local dev? No VPN needed — Bull Studio runs at http://zelly-bull.test and RedisInsight at http://zelly-redis.test via the docker-compose stack. See Local Development.

Step 1 — Install WireGuard

Do this once per machine. Pick your OS:

  1. Go to wireguard.com/install and download the Windows installer (.msi).
  2. Run the installer — no custom options needed. WireGuard appears in the system tray when running.
  3. Open a new PowerShell or Command Prompt window after install and verify:
    wg --version
i
The Windows installer bundles the GUI app and the wg / wg-quick CLI tools. The GUI is easiest for first-time setup; the CLI is useful in scripts.

Option A — GUI app (easiest)

  1. Open the Mac App Store and search for WireGuard (free, by WireGuard Development Team).
  2. Install it. The app appears in your Applications folder and adds a menu-bar icon.

Option B — CLI via Homebrew

bash
brew install wireguard-tools

wg --version   # verify
The GUI app and CLI tools coexist fine — most developers use the GUI to toggle daily and the CLI for scripting.
bash — Debian / Ubuntu
sudo apt update && sudo apt install -y wireguard wireguard-tools
wg --version
bash — Fedora / RHEL / Amazon Linux 2023
sudo dnf install -y wireguard-tools
wg --version
bash — Arch Linux
sudo pacman -S wireguard-tools

Step 2 — Create your VPN configuration

Each developer has a unique key pair and an assigned VPN IP in the 10.10.0.0/24 subnet. You generate your own private key — it never leaves your machine.

  1. Generate a key pair
    bash
    wg genkey | tee private.key | wg pubkey > public.key
    
    echo "Private key (keep secret):" && cat private.key
    echo ""
    echo "Public key (share with DevOps):" && cat public.key
    PowerShell — use Git Bash or WSL for key generation
    # Open Git Bash (ships with Git for Windows) or WSL and run:
    wg genkey | tee private.key | wg pubkey > public.key
    
    cat private.key   # goes in your config file — never share
    cat public.key    # share this with DevOps
    !
    Never share your private key. If it leaks, revoke it immediately — see Troubleshooting for the revocation steps.
  2. Get the bastion's server public key and public IP

    The bastion stores its own public key in SSM Parameter Store on first boot. Retrieve it:

    bash — production (ap-south-1)
    # Server public key
    SERVER_PUBKEY=$(aws ssm get-parameter \
      --name /zelly/bastion/wireguard-public-key \
      --region ap-south-1 \
      --query Parameter.Value --output text)
    
    # Bastion public IP
    BASTION_IP=$(aws ec2 describe-instances \
      --filters "Name=tag:Name,Values=zelly-production-bastion" \
                "Name=instance-state-name,Values=running" \
      --region ap-south-1 \
      --query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
    
    echo "Server public key: $SERVER_PUBKEY"
    echo "Bastion IP:        $BASTION_IP"
    PowerShell — production
    $SERVER_PUBKEY = aws ssm get-parameter `
      --name /zelly/bastion/wireguard-public-key `
      --region ap-south-1 `
      --query Parameter.Value --output text
    
    $BASTION_IP = aws ec2 describe-instances `
      --filters "Name=tag:Name,Values=zelly-production-bastion" `
                "Name=instance-state-name,Values=running" `
      --region ap-south-1 `
      --query "Reservations[0].Instances[0].PublicIpAddress" --output text
    
    Write-Host "Server public key: $SERVER_PUBKEY"
    Write-Host "Bastion IP:        $BASTION_IP"
  3. Register your public key

    Share your public key (from public.key) and your name with a team lead. You'll be assigned a VPN IP — for example, 10.10.0.3. The bastion config is updated via SSM RunCommand (no SSH required).

  4. Write your config file

    Create zelly-prod.conf (anywhere on your machine). Fill in the three placeholders:

    zelly-prod.conf
    [Interface]
    PrivateKey = <paste your private.key contents here>
    Address    = 10.10.0.<YOUR_ASSIGNED_NUMBER>/32
    
    [Peer]
    # Zelly production bastion
    PublicKey           = <server public key from Step 2>
    Endpoint            = <bastion public IP>:51820
    AllowedIPs          = 10.0.0.0/8
    PersistentKeepalive = 25

    AllowedIPs = 10.0.0.0/8 is a split tunnel — only 10.x.x.x traffic routes through the VPN. Your regular internet connection is unaffected. The VPC production CIDR is 10.0.0.0/16 and staging is 10.1.0.0/16 — both are covered by 10.0.0.0/8.

Step 3 — Connect to the VPN

  1. Open the WireGuard app (system tray or Start menu).
  2. Click Import tunnel(s) from file → select zelly-prod.conf.
  3. Click Activate. The status dot turns green when connected.
  4. Test:
    ping 10.10.0.1   # bastion VPN interface — should reply
  1. Open WireGuard.app from Applications.
  2. Click Import Tunnel(s) from File… → select zelly-prod.conf.
  3. Click Activate. The menu-bar icon turns solid.
  4. Test:
    ping 10.10.0.1
bash
# Connect (sudo required)
sudo wg-quick up ~/zelly-prod.conf

# Check handshake — confirm a recent handshake time
sudo wg show

# Test VPC connectivity
ping 10.10.0.1   # bastion VPN interface

# Disconnect when done
sudo wg-quick down ~/zelly-prod.conf
Copy the conf to /etc/wireguard/zelly-prod.conf (chmod 600) and then you can use just sudo wg-quick up zelly-prod without the full path.
!
Disconnect when you're done. Leaving a production VPN tunnel open indefinitely is a security risk — always toggle it off after your session.

Step 4 — Access resources

Keep the VPN active for all commands in this section.

Redis (ElastiCache)

Production ElastiCache requires TLS — plain-text connections are rejected. Get credentials from Secrets Manager, then connect:

bash — production
# Pull credentials
SECRET=$(aws secretsmanager get-secret-value \
  --secret-id zelly/redis/auth --region ap-south-1 \
  --query SecretString --output text)

HOST=$(echo "$SECRET" | jq -r .host)
TOKEN=$(echo "$SECRET" | jq -r .auth_token)

# Interactive CLI session
redis-cli -h "$HOST" -p 6379 --tls --no-auth-warning -a "$TOKEN"

# One-shot health check
redis-cli -h "$HOST" -p 6379 --tls -a "$TOKEN" PING

# Inspect BullMQ queues
redis-cli -h "$HOST" -p 6379 --tls -a "$TOKEN" KEYS "bull:*" | head -20
redis-cli -h "$HOST" -p 6379 --tls -a "$TOKEN" LLEN "bull:store-events:wait"
redis-cli -h "$HOST" -p 6379 --tls -a "$TOKEN" LLEN "bull:SHOPIFY_WEBHOOK:wait"
PowerShell — production
$r = aws secretsmanager get-secret-value `
  --secret-id zelly/redis/auth --region ap-south-1 `
  --query SecretString --output text | ConvertFrom-Json

# Connect with redis-cli (install via choco install redis-desktop-manager or WSL)
redis-cli -h $r.host -p 6379 --tls --no-auth-warning -a $r.auth_token

# Or display for use in a GUI client like RedisInsight
Write-Host "Host:  $($r.host)"
Write-Host "Port:  6379 (TLS on)"
Write-Host "Token: $($r.auth_token)"
!
Always pass --tls. ElastiCache enforces in-transit encryption — omitting --tls returns a protocol error, not a connection timeout.

Aurora MySQL (production)

Staging Aurora is publicly accessible — no VPN needed. For production, connect via VPN first.

StagingProduction
VPN required?No — public endpointYes — private subnet
Credentialszelly/aurora/master (ap-southeast-1)zelly/aurora/master (ap-south-1)
Userzellymaster
Databasesastro_primary, ecom_store_front, backoffice
bash — production (VPN on)
SECRET=$(aws secretsmanager get-secret-value \
  --secret-id zelly/aurora/master --region ap-south-1 \
  --query SecretString --output text)

HOST=$(echo "$SECRET" | jq -r .host)
PASS=$(echo "$SECRET" | jq -r .password)

mysql -h "$HOST" -u zellymaster -p"$PASS" astro_primary
bash — staging (no VPN needed)
SECRET=$(aws secretsmanager get-secret-value \
  --secret-id zelly/aurora/master --region ap-southeast-1 \
  --query SecretString --output text)

HOST=$(echo "$SECRET" | jq -r .host)
PASS=$(echo "$SECRET" | jq -r .password)

mysql -h "$HOST" -u zellymaster -p"$PASS" astro_primary
PowerShell — production (VPN on)
$s = aws secretsmanager get-secret-value `
  --secret-id zelly/aurora/master --region ap-south-1 `
  --query SecretString --output text | ConvertFrom-Json

Write-Host "Host: $($s.host)"
Write-Host "User: zellymaster"
Write-Host "Pass: $($s.password)"
# Paste these into MySQL Workbench, TablePlus, or your preferred GUI client

ClickHouse

ClickHouse runs on an EC2 instance (t3.medium, Docker) in the private subnet. The host is stored in the zelly/clickhouse/env secret. ClickHouse exposes two interfaces — HTTP (port 8123) and the native TCP protocol (port 9000).

bash — production (VPN on)
CH=$(aws secretsmanager get-secret-value \
  --secret-id zelly/clickhouse/env --region ap-south-1 \
  --query SecretString --output text)

CH_HOST=$(echo "$CH" | jq -r .host)
CH_PASS=$(echo "$CH" | jq -r .password)

# Ping the HTTP interface — should return "Ok."
curl "http://$CH_HOST:8123/ping"

# Run a query via HTTP (quickest for one-off checks)
curl "http://$CH_HOST:8123/?user=default&password=$CH_PASS" \
  --data "SELECT version()"

# Show databases
curl "http://$CH_HOST:8123/?user=default&password=$CH_PASS" \
  --data "SHOW DATABASES FORMAT Pretty"

# Interactive shell via native protocol (install clickhouse-client first)
clickhouse-client --host "$CH_HOST" --password "$CH_PASS" --port 9000
PowerShell — production (VPN on)
$ch = aws secretsmanager get-secret-value `
  --secret-id zelly/clickhouse/env --region ap-south-1 `
  --query SecretString --output text | ConvertFrom-Json

# Ping
Invoke-RestMethod "http://$($ch.host):8123/ping"

# Query via HTTP API
Invoke-RestMethod "http://$($ch.host):8123/?user=default&password=$($ch.password)" `
  -Method Post -Body "SELECT version()"

# Show databases
Invoke-RestMethod "http://$($ch.host):8123/?user=default&password=$($ch.password)" `
  -Method Post -Body "SHOW DATABASES"

Internal tools — direct VPN access

Bull Studio and RedisInsight are ECS Fargate tasks in private subnets. Their security group allows inbound on ports 3000 and 5540 from the bastion security group — WireGuard traffic is masqueraded through the bastion's private IP, so once the VPN is connected you can open these tools directly in a browser.

i
ECS Fargate tasks get a new private IP every time they restart (deploy, crash, manual redeploy). Re-run the IP lookup commands if a URL stops loading.

Bull Studio (BullMQ queue dashboard)

Shows all BullMQ queues — job counts, active workers, failed jobs, and retry history. Runs on port 3000.

  1. Connect WireGuard (Step 3 above — must be active).
  2. Look up the task's private IP
    bash — production
    TASK=$(aws ecs list-tasks \
      --cluster zelly-production \
      --service-name bull-studio \
      --region ap-south-1 \
      --query 'taskArns[0]' --output text)
    
    aws ecs describe-tasks \
      --cluster zelly-production \
      --tasks "$TASK" \
      --region ap-south-1 \
      --query 'tasks[0].attachments[0].details[?name==`privateIPv4Address`].value' \
      --output text
    PowerShell — production
    $TASK = aws ecs list-tasks `
      --cluster zelly-production --service-name bull-studio `
      --region ap-south-1 --query "taskArns[0]" --output text
    
    aws ecs describe-tasks `
      --cluster zelly-production --tasks $TASK `
      --region ap-south-1 `
      --query "tasks[0].attachments[0].details[?name=='privateIPv4Address'].value" `
      --output text
  3. Open in your browser

    Navigate to http://<task-private-ip>:3000 — you'll see all BullMQ queues with live job counts. For example: http://10.0.2.47:3000

RedisInsight (Redis GUI)

Visual key browser, stream viewer, and memory analysis for ElastiCache. Runs on port 5540. The Redis connection is pre-configured via REDIS_HOSTS — no manual setup in the UI.

  1. Connect WireGuard (must be active).
  2. Look up the task's private IP
    bash — production
    TASK=$(aws ecs list-tasks \
      --cluster zelly-production \
      --service-name redis-insight \
      --region ap-south-1 \
      --query 'taskArns[0]' --output text)
    
    aws ecs describe-tasks \
      --cluster zelly-production \
      --tasks "$TASK" \
      --region ap-south-1 \
      --query 'tasks[0].attachments[0].details[?name==`privateIPv4Address`].value' \
      --output text
    PowerShell
    $TASK = aws ecs list-tasks `
      --cluster zelly-production --service-name redis-insight `
      --region ap-south-1 --query "taskArns[0]" --output text
    
    aws ecs describe-tasks `
      --cluster zelly-production --tasks $TASK `
      --region ap-south-1 `
      --query "tasks[0].attachments[0].details[?name=='privateIPv4Address'].value" `
      --output text
  3. Open in your browser

    Navigate to http://<task-private-ip>:5540. The ElastiCache database will already appear in the sidebar — click it to start browsing keys.

Look up all internal tool IPs at once

bash — production
for SVC in bull-studio redis-insight; do
  TASK=$(aws ecs list-tasks --cluster zelly-production --service-name $SVC \
    --region ap-south-1 --query 'taskArns[0]' --output text)
  IP=$(aws ecs describe-tasks --cluster zelly-production --tasks "$TASK" \
    --region ap-south-1 \
    --query 'tasks[0].attachments[0].details[?name==`privateIPv4Address`].value' \
    --output text)
  echo "$SVC → $IP"
done
ToolURL pattern (VPN connected)What it shows
Bull Studiohttp://<task-ip>:3000BullMQ queues — job counts, retries, failed jobs
RedisInsighthttp://<task-ip>:5540Redis key browser, streams, memory usage

Troubleshooting

VPN activates but ping 10.10.0.1 gets no reply

1. Check that the handshake happened:

sudo wg show   # look for "latest handshake" — should be recent

No handshake means the bastion can't be reached on UDP 51820. Your public IP may not be in the bastion's security group allowlist. Contact DevOps to add your current public IP (curl ifconfig.me) to the bastion SG.

2. Check your AllowedIPs: it must include 10.0.0.0/8 or specifically 10.10.0.0/24 to route traffic to the bastion's VPN address.

VPN connected, ping works, but Redis / Aurora is unreachable

Your AllowedIPs may only cover 10.10.0.0/24 (the VPN subnet) instead of 10.0.0.0/8 (the full private range). Edit the conf file, set AllowedIPs = 10.0.0.0/8, then deactivate and re-activate the tunnel.

Also check that you are using the correct secret region: production secrets are in ap-south-1, staging in ap-southeast-1.

redis-cli error: ERR This instance has TLS enabled

You connected without --tls. Always include it:

redis-cli -h "$HOST" -p 6379 --tls --no-auth-warning -a "$TOKEN"
redis-cli: SSL_connect returned=1 (TLS build not supported)

Your redis-cli was compiled without OpenSSL. Reinstall from a TLS-enabled package:

# Ubuntu / Debian
sudo apt install redis-tools

# macOS
brew install redis   # Homebrew builds with TLS

# Verify TLS support
redis-cli --tls --version
Browser can't reach Bull Studio or RedisInsight IP

1. Is WireGuard connected? The VPN must be active. Check the app status or run sudo wg show — look for a recent latest handshake time.

2. Did the task restart? Fargate tasks get a new private IP on every restart. Re-run the IP lookup and use the new address.

3. Is the task running?

aws ecs describe-services \
  --cluster zelly-production \
  --services bull-studio redis-insight \
  --region ap-south-1 \
  --query 'services[*].{name:serviceName,running:runningCount,desired:desiredCount}' \
  --output table

If running = 0, force a redeploy: aws ecs update-service --cluster zelly-production --service bull-studio --region ap-south-1 --force-new-deployment

4. Terraform not applied yet? The SG ingress rules for ports 3000 and 5540 live in modules/security_groups/main.tf. If not yet applied, run terraform apply -target module.security_groups — SG changes take effect immediately, no task restart needed.

My private key was compromised — how do I revoke it?
  1. Deactivate the tunnel on your machine immediately.
  2. DevOps dashboard → VPN → find your entry → click Remove.
  3. Click Sync to Bastion — this removes your public key from /etc/wireguard/wg0.conf on the bastion via SSM RunCommand. No SSH access to the bastion is needed.
  4. Generate a new key pair and register the new public key as a fresh entry.