diff --git a/crialab.sh b/crialab.sh new file mode 100644 index 0000000..d2bb745 --- /dev/null +++ b/crialab.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# Infraestrutura do laboratorio. Compativel com curl -fsSL URL | bash. +# Configuracao por variaveis de ambiente; nao le perguntas de stdin. +set -Eeuo pipefail +export AWS_PAGER="" AWS_CLI_AUTO_PROMPT=off + +main() { + local mode="${1:---plan}" + case "$mode" in --plan|--apply) ;; *) echo 'Use --plan ou --apply' >&2; return 2;; esac + for tool in aws python3; do + command -v "$tool" >/dev/null || { echo "Falta instalar: $tool" >&2; return 2; } + done + REGION="${REGION:-us-east-1}" + LAB_NAME="${LAB_NAME:-modulo12-vpn}" + INSTANCE_TYPE="${INSTANCE_TYPE:-t3.micro}" + LOCAL_CIDR="${LOCAL_CIDR:-192.168.10.0/24}" + : "${KEY_NAME:?Defina KEY_NAME com um par de chaves EC2 existente na regiao.}" + : "${ADMIN_CIDR:?Defina ADMIN_CIDR com o IPv4 publico de administracao seguido de /32.}" + : "${VPN_PEER_CIDR:?Defina VPN_PEER_CIDR com o IPv4 publico de saida do laboratorio seguido de /32.}" + [[ "$LAB_NAME" =~ ^[a-zA-Z][a-zA-Z0-9-]{0,39}$ ]] || { echo 'LAB_NAME invalido' >&2; return 2; } + + # CIDRs canonicos evitam erros e sobreposicao com VPC/tunel planejados. + python3 - "$ADMIN_CIDR" "$VPN_PEER_CIDR" "$LOCAL_CIDR" <<'PY' +import ipaddress, sys +admin, peer, local = [ipaddress.IPv4Network(x, strict=True) for x in sys.argv[1:]] +for label, net in [('ADMIN_CIDR', admin), ('VPN_PEER_CIDR', peer)]: + if net.prefixlen != 32 or not net.network_address.is_global: + raise SystemExit(f'{label} deve ser um IPv4 publico /32 real.') +private = [ipaddress.ip_network(x) for x in ['10.0.0.0/8','172.16.0.0/12','192.168.0.0/16']] +if not any(local.subnet_of(x) for x in private): + raise SystemExit('LOCAL_CIDR deve ser uma rede privada RFC1918.') +for cidr in ['10.20.0.0/16', '10.8.0.0/24']: + if local.overlaps(ipaddress.ip_network(cidr)): + raise SystemExit(f'LOCAL_CIDR sobrepoe {cidr}.') +PY + aw() { aws --region "$REGION" "$@"; } + local account existing ami root_device az + account=$(aw sts get-caller-identity --query Account --output text) + aw ec2 describe-key-pairs --key-names "$KEY_NAME" >/dev/null + existing=$(aw ec2 describe-vpcs --filters "Name=tag:Lab,Values=$LAB_NAME" --query 'Vpcs[].VpcId' --output text) + if [[ -n "$existing" && "$existing" != None ]]; then + echo "Ja existe uma VPC deste laboratorio: $existing. Revise-a antes de executar novamente." >&2 + return 2 + fi + # Imagem oficial Canonical Ubuntu 24.04 LTS amd64, sem Marketplace. + ami=$(aw ec2 describe-images --owners 099720109477 --filters \ + 'Name=name,Values=ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*' \ + 'Name=state,Values=available' 'Name=architecture,Values=x86_64' \ + 'Name=root-device-type,Values=ebs' 'Name=virtualization-type,Values=hvm' \ + --query 'sort_by(Images,&CreationDate)[-1].ImageId' --output text) + [[ -n "$ami" && "$ami" != None ]] || { echo 'AMI Ubuntu nao encontrada.' >&2; return 2; } + root_device=$(aw ec2 describe-images --image-ids "$ami" --query 'Images[0].RootDeviceName' --output text) + az=$(aw ec2 describe-instance-type-offerings --location-type availability-zone \ + --filters "Name=instance-type,Values=$INSTANCE_TYPE" \ + --query 'sort_by(InstanceTypeOfferings,&Location)[0].Location' --output text) + [[ -n "$az" && "$az" != None ]] || { echo 'Tipo de instancia indisponivel na regiao.' >&2; return 2; } + cat < "$STATE_FILE" + trap 'echo "Falha na linha $LINENO. Recursos parciais podem existir; consulte $STATE_FILE e a tag Lab=$LAB_NAME. Nao houve rollback automatico." >&2' ERR + record() { printf '%s=%s\n' "$1" "$2" | tee -a "$STATE_FILE"; } + tags() { printf 'ResourceType=%s,Tags=[{Key=Lab,Value=%s},{Key=Name,Value=%s-%s}]' "$1" "$LAB_NAME" "$LAB_NAME" "$2"; } + local vpc igw public_subnet private_subnet public_rt private_rt gateway_sg services_sg gateway server + vpc=$(aw ec2 create-vpc --cidr-block 10.20.0.0/16 --tag-specifications "$(tags vpc vpc)" --query Vpc.VpcId --output text) + record VpcId "$vpc" + aw ec2 wait vpc-available --vpc-ids "$vpc" + aw ec2 modify-vpc-attribute --vpc-id "$vpc" --enable-dns-support '{"Value":true}' + aw ec2 modify-vpc-attribute --vpc-id "$vpc" --enable-dns-hostnames '{"Value":true}' + igw=$(aw ec2 create-internet-gateway --tag-specifications "$(tags internet-gateway igw)" --query InternetGateway.InternetGatewayId --output text) + record InternetGatewayId "$igw" + aw ec2 attach-internet-gateway --internet-gateway-id "$igw" --vpc-id "$vpc" + public_subnet=$(aw ec2 create-subnet --vpc-id "$vpc" --cidr-block 10.20.1.0/24 --availability-zone "$az" --tag-specifications "$(tags subnet publica)" --query Subnet.SubnetId --output text) + record PublicSubnetId "$public_subnet" + private_subnet=$(aw ec2 create-subnet --vpc-id "$vpc" --cidr-block 10.20.2.0/24 --availability-zone "$az" --tag-specifications "$(tags subnet privada)" --query Subnet.SubnetId --output text) + record PrivateSubnetId "$private_subnet" + public_rt=$(aw ec2 create-route-table --vpc-id "$vpc" --tag-specifications "$(tags route-table publica)" --query RouteTable.RouteTableId --output text) + record PublicRouteTableId "$public_rt" + aw ec2 associate-route-table --route-table-id "$public_rt" --subnet-id "$public_subnet" >/dev/null + aw ec2 create-route --route-table-id "$public_rt" --destination-cidr-block 0.0.0.0/0 --gateway-id "$igw" >/dev/null + private_rt=$(aw ec2 create-route-table --vpc-id "$vpc" --tag-specifications "$(tags route-table privada)" --query RouteTable.RouteTableId --output text) + record PrivateRouteTableId "$private_rt" + aw ec2 associate-route-table --route-table-id "$private_rt" --subnet-id "$private_subnet" >/dev/null + gateway_sg=$(aw ec2 create-security-group --vpc-id "$vpc" --group-name "$LAB_NAME-gateway" --description 'Gateway OpenVPN do laboratorio' --tag-specifications "$(tags security-group gateway)" --query GroupId --output text) + record GatewaySecurityGroupId "$gateway_sg" + services_sg=$(aw ec2 create-security-group --vpc-id "$vpc" --group-name "$LAB_NAME-servicos" --description 'Servicos privados do laboratorio' --tag-specifications "$(tags security-group servicos)" --query GroupId --output text) + record ServicesSecurityGroupId "$services_sg" + allow() { aw ec2 authorize-security-group-ingress --group-id "$1" --protocol "$2" --port "$3" --cidr "$4" >/dev/null; } + allow "$gateway_sg" tcp 22 "$ADMIN_CIDR" + allow "$gateway_sg" udp 1194 "$VPN_PEER_CIDR" + # Pacotes dos servidores atravessam o gateway para chegar a rede local. + aw ec2 authorize-security-group-ingress --group-id "$gateway_sg" --ip-permissions \ + '[{"IpProtocol":"-1","IpRanges":[{"CidrIp":"10.20.2.0/24","Description":"Encaminhamento da sub-rede privada para VPN"}]}]' >/dev/null + local cidr + for cidr in "$LOCAL_CIDR" 10.8.0.0/24 10.20.1.10/32; do + allow "$services_sg" tcp 22 "$cidr" + allow "$services_sg" tcp 80 "$cidr" + allow "$services_sg" icmp -1 "$cidr" + done + # Sem pacotes externos: bootstrap funciona inclusive na sub-rede privada isolada. + local gateway_user_data='#cloud-config +write_files: + - path: /etc/sysctl.d/90-vpn-lab.conf + permissions: "0644" + content: | + net.ipv4.ip_forward=1 +runcmd: + - [sysctl, --system] +' + local disk="[{\"DeviceName\":\"$root_device\",\"Ebs\":{\"VolumeSize\":8,\"VolumeType\":\"gp3\",\"DeleteOnTermination\":true,\"Encrypted\":true}}]" + gateway=$(aw ec2 run-instances --image-id "$ami" --instance-type "$INSTANCE_TYPE" --count 1 \ + --key-name "$KEY_NAME" --subnet-id "$public_subnet" --private-ip-address 10.20.1.10 \ + --security-group-ids "$gateway_sg" --no-associate-public-ip-address \ + --metadata-options HttpTokens=required,HttpEndpoint=enabled \ + --block-device-mappings "$disk" --user-data "$gateway_user_data" \ + --tag-specifications "$(tags instance gateway)" "$(tags volume gateway)" \ + --client-token "$vpc-gateway" --query 'Instances[0].InstanceId' --output text) + record GatewayInstanceId "$gateway" + aw ec2 wait instance-running --instance-ids "$gateway" + # Registrar a alocacao antes da associacao permite recuperar uma falha parcial. + local eip_allocation eip_association eip_target public_ip + eip_allocation=$(aw ec2 allocate-address --domain vpc \ + --tag-specifications "$(tags elastic-ip gateway)" --query AllocationId --output text) + record ElasticIpAllocationId "$eip_allocation" + public_ip=$(aw ec2 describe-addresses --allocation-ids "$eip_allocation" --query 'Addresses[0].PublicIp' --output text) + record GatewayPublicIp "$public_ip" + eip_association=$(aw ec2 associate-address --allocation-id "$eip_allocation" \ + --instance-id "$gateway" --no-allow-reassociation --query AssociationId --output text) + record ElasticIpAssociationId "$eip_association" + eip_target=$(aw ec2 describe-addresses --allocation-ids "$eip_allocation" --query 'Addresses[0].InstanceId' --output text) + [[ "$eip_target" == "$gateway" ]] || { echo 'Elastic IP nao associado ao gateway esperado.' >&2; return 1; } + aw ec2 modify-instance-attribute --instance-id "$gateway" --no-source-dest-check + aw ec2 create-route --route-table-id "$private_rt" --destination-cidr-block "$LOCAL_CIDR" --instance-id "$gateway" >/dev/null + aw ec2 create-route --route-table-id "$private_rt" --destination-cidr-block 10.8.0.0/24 --instance-id "$gateway" >/dev/null + server=$(aw ec2 run-instances --image-id "$ami" --instance-type "$INSTANCE_TYPE" --count 1 \ + --key-name "$KEY_NAME" --subnet-id "$private_subnet" --private-ip-address 10.20.2.10 \ + --security-group-ids "$services_sg" --no-associate-public-ip-address \ + --metadata-options HttpTokens=required,HttpEndpoint=enabled --block-device-mappings "$disk" \ + --tag-specifications "$(tags instance servicos)" "$(tags volume servicos)" \ + --client-token "$vpc-servicos" --query 'Instances[0].InstanceId' --output text) + record ServicesInstanceId "$server" + aw ec2 wait instance-running --instance-ids "$server" + local private_public_ip + private_public_ip=$(aw ec2 describe-instances --instance-ids "$server" --query 'Reservations[0].Instances[0].PublicIpAddress' --output text) + [[ "$private_public_ip" == None || -z "$private_public_ip" ]] || { echo 'Servidor privado recebeu IP publico inesperado.' >&2; return 1; } + cat <