#!/bin/bash # vault1984 POP Deploy Script — canonical, idempotent # Usage: ./deploy-pop.sh # Example: ./deploy-pop.sh virginia us-east-1 set -euo pipefail NODE_ID="${1:?usage: deploy-pop.sh }" REGION="${2:?usage: deploy-pop.sh }" HQ_URL="http://185.218.204.47:8080" IAM_PROFILE="vault1984-ssm-profile" INSTANCE_TYPE="t4g.micro" echo "=== vault1984 POP Deploy: $NODE_ID in $REGION ===" # 1. Find latest AL2 arm64 AMI echo "[1/6] Finding AMI..." AMI_ID=$(aws --region "$REGION" ec2 describe-images \ --owners amazon \ --filters "Name=name,Values=amzn2-ami-kernel-5.10-hvm-*-arm64-gp2" "Name=state,Values=available" \ --query "sort_by(Images, &CreationDate)[-1].ImageId" --output text) echo " AMI: $AMI_ID" # 2. Get/create vault1984-pop security group (outbound only, NO inbound) echo "[2/6] Security group..." VPC_ID=$(aws --region "$REGION" ec2 describe-vpcs \ --filters "Name=isDefault,Values=true" --query "Vpcs[0].VpcId" --output text) SUBNET_ID=$(aws --region "$REGION" ec2 describe-subnets \ --filters "Name=defaultForAz,Values=true" --query "Subnets[0].SubnetId" --output text) # Look up existing SG — treat empty or literal "None" as missing SG_ID=$(aws --region "$REGION" ec2 describe-security-groups \ --filters "Name=group-name,Values=vault1984-pop" "Name=vpc-id,Values=$VPC_ID" \ --query "SecurityGroups[0].GroupId" --output text 2>/dev/null || true) if [[ -z "$SG_ID" || "$SG_ID" == "None" ]]; then echo " Creating vault1984-pop SG..." SG_ID=$(aws --region "$REGION" ec2 create-security-group \ --group-name vault1984-pop \ --description "Vault1984 POP - outbound only, no inbound" \ --vpc-id "$VPC_ID" --query "GroupId" --output text) # Remove the default allow-all inbound rule AWS adds to new SGs aws --region "$REGION" ec2 revoke-security-group-ingress \ --group-id "$SG_ID" --protocol -1 --cidr 0.0.0.0/0 2>/dev/null || true aws --region "$REGION" ec2 revoke-security-group-ingress \ --group-id "$SG_ID" --protocol -1 --source-group "$SG_ID" 2>/dev/null || true echo " Created SG: $SG_ID" else echo " Existing SG: $SG_ID" fi # Verify SG has no inbound rules (safety check) INBOUND=$(aws --region "$REGION" ec2 describe-security-groups \ --group-ids "$SG_ID" --query "SecurityGroups[0].IpPermissions" --output text) if [[ -n "$INBOUND" ]]; then echo " WARNING: vault1984-pop SG has inbound rules — check manually:" echo " $INBOUND" fi # 3. Launch instance echo "[3/6] Launching $INSTANCE_TYPE..." INSTANCE_ID=$(aws --region "$REGION" ec2 run-instances \ --image-id "$AMI_ID" \ --instance-type "$INSTANCE_TYPE" \ --subnet-id "$SUBNET_ID" \ --security-group-ids "$SG_ID" \ --iam-instance-profile Name="$IAM_PROFILE" \ --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=vault1984-$NODE_ID},{Key=vault1984-node,Value=$NODE_ID}]" \ --query "Instances[0].InstanceId" --output text) echo " Instance: $INSTANCE_ID" # Verify SG assignment immediately after launch ASSIGNED_SG=$(aws --region "$REGION" ec2 describe-instances \ --instance-ids "$INSTANCE_ID" \ --query "Reservations[0].Instances[0].SecurityGroups[0].GroupName" --output text) if [[ "$ASSIGNED_SG" != "vault1984-pop" ]]; then echo "ERROR: Instance launched with wrong SG: $ASSIGNED_SG (expected vault1984-pop)" echo " Terminating instance to avoid misconfigured POP." aws --region "$REGION" ec2 terminate-instances --instance-ids "$INSTANCE_ID" >/dev/null exit 1 fi echo " SG verified: $ASSIGNED_SG ✓" # 4. Wait for SSM echo "[4/6] Waiting for SSM (~2-3 min)..." PUBLIC_IP="" for i in $(seq 1 30); do STATUS=$(aws --region "$REGION" ssm describe-instance-information \ --filters "Key=InstanceIds,Values=$INSTANCE_ID" \ --query "InstanceInformationList[0].PingStatus" --output text 2>/dev/null || echo "None") if [[ "$STATUS" == "Online" ]]; then PUBLIC_IP=$(aws --region "$REGION" ec2 describe-instances \ --instance-ids "$INSTANCE_ID" \ --query "Reservations[0].Instances[0].PublicIpAddress" --output text) echo " SSM online after ~${i}0s — IP: $PUBLIC_IP" break fi echo " ($i/30) $STATUS..." sleep 10 done [[ "$STATUS" == "Online" ]] || { echo "ERROR: SSM never came online"; exit 1; } # 5. Install vault1984 + apply hardening inline (same approach as update-pop.sh) echo "[5/6] Deploying vault1984 and hardening..." CMD_ID=$(aws --region "$REGION" ssm send-command \ --instance-ids "$INSTANCE_ID" \ --document-name AWS-RunShellScript \ --parameters commands="[ \"set -e\", \"hostnamectl set-hostname $NODE_ID\", \"curl -sfo /usr/local/bin/vault1984 $HQ_URL/download/vault1984-arm64\", \"chmod +x /usr/local/bin/vault1984\", \"mkdir -p /var/lib/vault1984\", \"printf '[Unit]\\nDescription=Vault1984\\nAfter=network.target\\n\\n[Service]\\nEnvironment=NODE_ID=$NODE_ID\\nExecStart=/usr/local/bin/vault1984 --telemetry-freq=60 --telemetry-host=$HQ_URL/telemetry\\nRestart=always\\nRestartSec=10\\nWorkingDirectory=/var/lib/vault1984\\n\\n[Install]\\nWantedBy=multi-user.target\\n' > /etc/systemd/system/vault1984.service\", \"systemctl daemon-reload && systemctl enable vault1984 && systemctl start vault1984\", \"systemctl is-active vault1984 && echo 'vault1984: OK' || echo 'vault1984: FAILED'\", \"amazon-linux-extras install epel -y -q > /dev/null 2>&1 || true\", \"yum install -y -q fail2ban > /dev/null 2>&1 || true\", \"printf '[DEFAULT]\\nbantime = 86400\\nfindtime = 600\\nmaxretry = 3\\nignoreip = 127.0.0.1/8 ::1\\n\\n[sshd]\\nenabled = true\\nport = ssh\\nfilter = sshd\\nlogpath = /var/log/secure\\nmaxretry = 3\\nbantime = 86400\\n' > /etc/fail2ban/jail.local\", \"systemctl enable fail2ban && systemctl restart fail2ban\", \"sleep 5 && fail2ban-client status sshd\", \"for svc in postfix rpcbind sshd; do systemctl stop \$svc 2>/dev/null; systemctl disable \$svc 2>/dev/null; done\", \"yum install -y -q chrony > /dev/null 2>&1 && systemctl enable chronyd && systemctl start chronyd || true\", \"printf 'net.ipv4.tcp_syncookies=1\\nnet.ipv4.icmp_echo_ignore_broadcasts=1\\nnet.ipv4.conf.all.rp_filter=1\\n' > /etc/sysctl.d/99-vault1984.conf && sysctl --system -q\", \"systemctl enable firewalld && systemctl start firewalld\", \"firewall-cmd --permanent --remove-service=ssh 2>/dev/null || true\", \"firewall-cmd --permanent --add-port=1984/tcp && firewall-cmd --reload\", \"echo 'DEPLOY_COMPLETE'\" ]" \ --query "Command.CommandId" --output text) for i in $(seq 1 30); do R=$(aws --region "$REGION" ssm get-command-invocation \ --command-id "$CMD_ID" --instance-id "$INSTANCE_ID" \ --query "{S:Status,O:StandardOutputContent,E:StandardErrorContent}" --output json 2>/dev/null || echo '{"S":"Pending"}') S=$(echo "$R" | python3 -c "import json,sys; print(json.load(sys.stdin).get('S','?'))") if [[ "$S" == "Success" || "$S" == "Failed" || "$S" == "TimedOut" ]]; then echo "$R" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('O','')); e=d.get('E',''); e and print('STDERR:',e)" [[ "$S" == "Success" ]] || { echo "ERROR: deploy command $S"; exit 1; } break fi echo " ($i/30) $S..." sleep 10 done # 6. Final verification echo "[6/6] Verification..." VERIFY_ID=$(aws --region "$REGION" ssm send-command \ --instance-ids "$INSTANCE_ID" \ --document-name AWS-RunShellScript \ --parameters commands="[ \"echo 'hostname:' $(hostname)\", \"systemctl is-active vault1984 && echo 'vault1984: active' || echo 'vault1984: FAILED'\", \"systemctl is-active fail2ban && echo 'fail2ban: active' || echo 'fail2ban: FAILED'\", \"fail2ban-client status sshd 2>/dev/null && echo 'fail2ban-sshd: OK' || echo 'fail2ban-sshd: FAILED'\", \"firewall-cmd --list-ports 2>/dev/null | grep -q 1984 && echo 'firewall: OK' || echo 'firewall: FAILED'\", \"systemctl is-active rpcbind 2>/dev/null && echo 'rpcbind: WARNING still active' || echo 'rpcbind: disabled OK'\" ]" \ --query "Command.CommandId" --output text) sleep 10 aws --region "$REGION" ssm get-command-invocation \ --command-id "$VERIFY_ID" --instance-id "$INSTANCE_ID" \ --query "StandardOutputContent" --output text echo "" echo "=== Deploy complete ===" echo " Node: $NODE_ID" echo " Region: $REGION" echo " Instance: $INSTANCE_ID" echo " IP: $PUBLIC_IP" echo " SG: vault1984-pop ($SG_ID)" echo "" echo "Add to POPS in update-pop.sh:" echo " [\"$NODE_ID\"]=\"$REGION:$INSTANCE_ID\""