Колись вже трохи писав про Pritunl – Pritunl: запуск VPN в Kubernetes.
Повернемось до цієї теми ще раз, але цього разу на EC2 в AWS, без Kubernetes.
Отже, що треба – це запустити якийсь VPN-сервіс для проекту, або мати доступ до всяких Kubernetes API/Kubernetes WorkerNodes/AWS RDS у приватних мережах.
Вибір тут, в принципі, є – і AWS VPN, і ванільний OpenVPN, і багато іншого.
Але я Pritunl користувався вже в декількох проектах, він має приємний інтерфейс, основні можливості VPN доступні у Free версії – тож вай нот?
Зміст
Що таке Pritunl
Фактично, Pritunl – це обгортка над звичайним OpenVPN сервером. Повністю сумісний, використовує однакові конфіги, і так далі.
Вміє в інтеграцію з AWS VPC – https://pritunl.com/vpc, але мені не дуже хочеться, щоб хтось автоматом міняв таблиці маршрутизації.
Сетап мережі в AWS у нас доволі простий, і поки що все можна менеджити самому – більше контролю, більше розуміння, що може піти не так.
Ну і плюс ця інтеграція наче доступна тільки в Enerprize – Pritunl Pricing.
У Pritunl дві основні концепції – Organization та Server:
- Server описує конфіг для OpenVPN – порти, роути, DNS
- Organization описує юзерів
- Organization підключається до Server
Далі, юзер завантажує файл .ovpn
, і підключається будь-яким VPN-клієнтом. Наскільки пам’ятаю, навіть дефолтний клієнт на macOS працював з ним без проблем.
Pritunl та Terraform
На попередньому проекті ми мали Pritunl в Kubernetes, але мені ця ідея якось не дуже подобається, бо, імхо, VPN має бути окремим сервісом.
Якщо говорити про Terraform, то є цікавий Pritunl Provider – але йому потрібен API ключ, який в Pritunl доступний тільки в Enerprize.
Ще є готовий код з Terraform тут – Pritunl VPN, але мені якось простіше підняти власну EC2 у власній VPC.
І ще нагуглив такий готовий модуль – AWS VPN (Pritunl) Terraform Module, виглядає наче робочим рішенням.
Проте будемо робити більш дідовським методом:
- є звичайна AWS VPC з кількома приватними сабнетами
- у публічному сабнеті з Terraform запустимо звичайний EC2
- через AWS EC2
user_data
встановимо і запустимо Pritunl - і вручну налаштуємо йому юзерів, сервери, роути
Роутинг має бути таким: всі пакети, які йдуть у VPC – відправляються через VPN, а решта – через звичайне підключення.
Terraform: створення EC2
Отже, спершу нам потрібно запустити EC2, на якій буде працювати Pritunl.
Для цього EC2 нам потрібні AWS AMI, SSK Key, Security Group, VPC ID, і відразу створимо AWS Route53 запис.
Отримання VPC ID
VPC ID отримуємо через terraform_remote_state
, детальніше писав у Terraform: terraform_remote_state – отримання outputs інших state-файлів:
data "terraform_remote_state" "vpc" { backend = "s3" config = { bucket = "tf-state-backend-atlas-vpc" key = "${var.environment}/atlas-vpc-${var.environment}.tfstate" region = var.aws_region dynamodb_table = "tf-state-lock-atlas-vpc" } }
У цьому outputs
маємо VPC ID та ID публічних сабнетів:
$ terraform output ... vpc_id = "vpc-0fbaffe234c0d81ea" ... vpc_public_subnets_cidrs = tolist([ "10.0.0.0/20", "10.0.16.0/20", ]) vpc_public_subnets_ids = [ "subnet-01de26778bea10395", "subnet-0efd3937cadf669d4", ]
І далі цей data resource використовуємо у locals
:
locals { # get VPC info vpc_out = data.terraform_remote_state.vpc.outputs }
Хоча в принципі теж можна зробити просто з data "aws_vpc"
.
EC2 SSH Key
Використовуємо key_pair
.
Створюємо сам ключ:
$ ssh-keygen Generating public/private ed25519 key pair. Enter file in which to save the key (/home/setevoy/.ssh/id_ed25519): /home/setevoy/.ssh/atlas-vpn ...
Публічну частину можемо зберігати в репозиторії – створюємо каталог, і копіюємо її:
$ mkdir ssh $ cp /home/setevoy/.ssh/atlas-vpn.pub ssh/
Описуємо ресурс aws_key_pair
:
resource "aws_key_pair" "vpn_key" { key_name = "atlas-vpn-key" public_key = file("${path.module}/ssh/atlas-vpn.pub") }
AWS Secuirty Group
Знаходимо домашній/робочий IP:
$ curl ifconfig.me 178.***.***.52
Описуємо SecurityGroup – дозволяємо SSH тільки з цього IP, у vpc_id
використовуємо local.vpc_out.vpc_id
.
Додаємо порти – 80 для Let’s Encrypt, який використовується Pritunl, 443 – для доступу до його адмінки, тут знов тільки з мого IP, 10052 UPD – для клієнтів VPN:
resource "aws_security_group" "allow_ssh" { name = "allow_ssh" description = "Allow SSH inbound traffic" vpc_id = local.vpc_out.vpc_id ingress { description = "SSH Arseny home" from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["178.***.***.52/32"] } ingress { description = "Pritunl Admin Arseny home" from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["178.***.***.52/32"] } ingress { description = "Pritunl Lets Encrypt" from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { description = "Pritunl VPN port" from_port = 10052 to_port = 10052 protocol = "udp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "${var.project_name}-${var.environment}-allow_ssh" } }
AWS AMI
Використовуючи data "aws_ami"
, знайдемо AWS AMI з Ubuntu.
Я спочатку пробував Pritunl запустити на Amazon Linux, але той yum
і окремі репозиторії – це якась біда, на Ubuntu завелось без проблем:
data "aws_ami" "ubuntu" { most_recent = true filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"] } filter { name = "virtualization-type" values = ["hvm"] } owners = ["099720109477"] # Canonical's official AWS account ID for Ubuntu AMIs }
Але використовуючи data "aws_ami"
майте на увазі, що коли вийде якась обнова, то AWS створить новий AMI, і при наступному запуску вашого Terraform-коду він підтягне новий ID, і запропонує перестворити відповідну EC2.
Тому, можливо, краще просто знайти AMI ID вручну і занести в variables. Див. Find an AMI та Amazon EC2 AMI Locator.
AWS Elastic IP
Аби мати одну і ту ж саму адресу, зробимо її окремим ресурсом:
resource "aws_eip" "vpn_eip" { domain = "vpc" }
AWS Route53 VPN record
Відразу створимо запис в DNS.
В variables.tf
задаємо ID зони в Route53 та її ім’я:
variable "route53_ops_zone" { type = object({ name = string id = string }) default = { name = "ops.example.co" id = "Z02***OYY" } }
І в main.tf
описуємо сам запис:
resource "aws_route53_record" "vpn_dns" { zone_id = var.route53_ops_zone.id name = "vpn.${var.route53_ops_zone.name}" type = "A" ttl = 300 records = [aws_eip.vpn_eip.public_ip] }
Тепер у нас буде запис виду “vpn.ops.example.co IN A <EC2_EIP>
.
AWS EC2 та установка Pritunl
І, врешті-решт, описуємо сам EC2, використовуючи ресурси, які створили вище:
ami
– беремо зdata.aws_ami.amazon_linu
key_name
– беремо зaws_key_pair.vpn_key.key_name
-
vpc_security_group_ids
– з SG, яку створюємо тут же -
subnet_id
, де створювати EС2 – беремо зlocal.vpc_out.vpc_public_subnets_ids
Тут же відразу додаємо встановлення Pritunl – див. документацію [Other Providers] Ubuntu 22.04, але вона місцями крива, тому, можливо, краще зробити установку руками після створення інстансу.
Ну, або таки додати в user_data
– принаймні на момент написання з кодом нижче це працювало.
У випадку проблем з EC2 user_data
– перевіряйте лог /var/log/cloud-init.log
, і спробуйте запустити скрипт вручну – він має бути у файлі типу /var/lib/cloud/instance/scripts/part-001
.
Майте на увазі, що user_data
викликається тільки при створенні інстансу:
resource "aws_instance" "vpn" { ami = data.aws_ami.ubuntu.id instance_type = var.vpn_ec2_instance_type key_name = aws_key_pair.vpn_key.key_name vpc_security_group_ids = [aws_security_group.allow_ssh.id] subnet_id = local.vpc_out.vpc_public_subnets_ids[0] user_data = <<-EOF #!/bin/bash echo 'deb http://repo.pritunl.com/stable/apt jammy main' > /etc/apt/sources.list.d/pritunl.list echo 'deb https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse' > /etc/apt/sources.list.d/mongodb-org-6.0.list apt-key adv --keyserver hkp://keyserver.ubuntu.com --recv 7568D9BB55FF9E5287D586017AE645C0CF8E292A wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add - apt update apt --assume-yes upgrade apt -y install wireguard wireguard-tools ufw disable apt -y install pritunl mongodb-org systemctl enable mongod pritunl systemctl start mongod pritunl EOF tags = { Name = "Pritunl VPN" } }
Додаємо підключення Elastic IP до цього інстансу:
resource "aws_eip_association" "vpn_eip_assoc" { instance_id = aws_instance.vpn.id allocation_id = aws_eip.vpn_eip.id }
Terraform Outputs
Додамо outputs
, аби потім простіше було шукати всякі ID:
output "vpn_ec2_id" { value = aws_instance.vpn.id } output "vpn_eip" { value = aws_eip.vpn_eip.public_ip } output "aws_ami_id" { value = data.aws_ami.ubuntu.id } output "vpn_dns" { value = aws_route53_record.vpn_dns.name }
Робимо terraform init
, terraform plan
, terraform apply
:
... Apply complete! Resources: 2 added, 0 changed, 0 destroyed. Outputs: ec2_public_ip = "3.83.69.105" vpn_ec2_id = "i-0ea1407cb7ff8690f"
Перевіряємо інстанс:
Перевіряємо SSH до нього:
$ ssh -i ~/.ssh/atlas-vpn [email protected] ... [ec2-user@ip-10-0-3-26 ~]$ sudo -s [root@ip-10-0-3-26 ec2-user]#
Перевіряємо сам Pritunl на сервері:
root@ip-10-0-1-25:/home/ubuntu# systemctl status pritunl ● pritunl.service - Pritunl Daemon Loaded: loaded (/etc/systemd/system/pritunl.service; enabled; vendor preset: enabled) Active: active (running) since Fri 2024-05-31 13:04:08 UTC; 55s ago Main PID: 3812 (pritunl) Tasks: 19 (limit: 2328) Memory: 99.7M CPU: 1.318s CGroup: /system.slice/pritunl.service ├─3812 /usr/lib/pritunl/usr/bin/python3 /usr/lib/pritunl/usr/bin/pritunl start └─4174 pritunl-web May 31 13:04:08 ip-10-0-1-25 systemd[1]: Started Pritunl Daemon.
Тепер можна переходити до його налаштування.
Pritunl inital setup
Документація – Configuration.
Підключаємось на EC2, виконуємо pritunl setup-key
:
root@ip-10-0-1-25:/home/ubuntu# pritunl setup-key 074d9be70f1944d7a77374cca09ff8dc
Відкриваємо vpn.ops.example.co:443
, на помилку ERR_CERT_AUTHORITY_INVALID
поки не звертаємо уваги – Let’s Encrypt згенерить сертифікат після налаштувань Pritunl.
Передаємо setup-key, адресу MongoDB можна лишити по дефолту:
Чекаємо апдейту MongoDB:
Коли відкриється вікно логіна – на сервері виконуємо pritunl default-password
:
root@ip-10-0-1-25:/home/ubuntu# pritunl default-password [local][2024-05-31 13:12:41,687][INFO] Getting default administrator password Administrator default password: username: "pritunl" password: "1rueBHeV9LIj"
І логінимось:
Генеруємо новий пароль, яким будемо користуватись вже постійно:
$ pwgen 12 1 iBai1Aisheat
І задаємо основні параметри Pritunl – тут тільки логін/пароль та адреси:
Якщо забули новий пароль – можна скинути з pritunl reset-password
.
Error getting LetsEncrypt certificate check the logs for more information.
При проблемах з Let’s Ecnrypt – перевіряємо лог /var/log/pritunl.log
, там все буде:
root@ip-10-0-1-25:/home/ubuntu# tail -f /var/log/pritunl.log File "/usr/lib/pritunl/usr/lib/python3.9/site-packages/pritunl/handlers/settings.py", line 1112, in settings_put acme.update_acme_cert() File "/usr/lib/pritunl/usr/lib/python3.9/site-packages/pritunl/acme.py", line 73, in update_acme_cert cert = get_acme_cert(settings.app.acme_key, csr, cmdline) File "/usr/lib/pritunl/usr/lib/python3.9/site-packages/pritunl/acme.py", line 45, in get_acme_cert certificate = acmetiny.get_crt( File "/usr/lib/pritunl/usr/lib/python3.9/site-packages/pritunl/acmetiny.py", line 138, in get_crt raise ValueError("Challenge did not pass for {0}: {1}".format(domain, authorization)) ValueError: Challenge did not pass for vpn.ops.example.co: {'identifier': {'type': 'dns', 'value': 'vpn.ops.example.co'}, 'status': 'invalid', 'expires': '2024-06-07T13:32:30Z', 'challenges': [{'type': 'http-01', 'status': 'invalid', 'error': {'type': 'urn:ietf:params:acme:error:dns', 'detail': 'DNS problem: NXDOMAIN looking up A for vpn.ops.example.co - check that a DNS record exists for this domain; DNS problem: NXDOMAIN looking up AAAA for vpn.ops.example.co - check that a DNS record exists for this domain', 'status': 400}, 'url': 'https://acme-v02.api.letsencrypt.org/acme/chall-v3/357864308812/RHhMwA', 'token': 'KZLx4dUxDmow5uMvfJdwbgz5bY4HG0tTQOW2m4UvFBg', 'validated': '2024-05-31T13:32:30Z'}]} acme_domain = "vpn.ops.example.co"
Домен новий – Let’s Encrypt про нього ще не знає.
Чекаємо кілька хвилин, і пробуємо ще раз.
Успішна реєстрація сертифікату в логах має виглядати так:
[INFO] Found domains: vpn.ops.example.co [INFO] Getting directory... [INFO] Directory found! [INFO] Registering account... [INFO] Registered! [INFO] Creating new order... [INFO] Order created! [INFO] Verifying vpn.ops.example.co... [INFO] vpn.ops.example.co verified! [INFO] Signing certificate... [INFO] Certificate signed! [INFO] Settings changed, restarting server...
Створення Pritunl Organization та юзерів
Додаємо організацію – через неї будемо групувати юзерів, бо Groups в безплатний версії недоступні:
Додаємо юзера:
Email, Pin – опціональні, зараз не потрібні:
Створення Pritunl Server та роути
Див. Server configuration.
Переходимо до Servers, додаємо новий:
В DNS Server задаємо адресу DNS нашої VPC.
В Port задаємо порт, який відкривали на AWS EC2 SecurityGroup.
Virtual Network – пул, з якого будуть виділятись адреси клієнтам. Я тут використовую 172.*, бо простіше відрізнити від решти – дома 192.*, VPC 10.*.
Підключаємо створену раніше Організацію:
Стартуємо сервер:
Налаштуємо роути – щоб через VPN йшли запити тільки в VPC:
І видаляємо дефолтний роут в 0.0.0.0/0
:
Linux OpenVPN – підключення до серверу
Переходимо в Users, клікаємо Download profile:
Розпаковуємо його:
$ tar xfpv test-user.tar org-all_test-user_org-all-serv.ovpn
І підключаємося за допомогою звичайного OpenVPN клієнта:
$ sudo openvpn --config org-all_test-user_org-all-serv.ovpn
У випадку помилки “ERROR: Cannot open TUN/TAP dev /dev/net/tun: No such device” на Linux – спробуйте перезавантажитись. В мене ядро було оновлене, і давно не ребутався.
Перевіряємо локальні роути:
$ route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.3.1 0.0.0.0 UG 600 0 0 wlan0 0.0.0.0 192.168.3.1 0.0.0.0 UG 1002 0 0 enp2s0f0 10.0.0.0 172.16.0.1 255.255.0.0 UG 0 0 0 tun0 172.16.0.0 0.0.0.0 255.255.255.0 U 0 0 0 tun0 ...
Все гуд – в Інтернет, 0.0.0.0
, ходимо старим маршрутом, через домашній роутер, а у VPC, 10.0.0.0
– через 172.16.0.1
, наш VPN.
Спробуємо:
$ traceroute 1.1.1.1 traceroute to 1.1.1.1 (1.1.1.1), 30 hops max, 60 byte packets 1 _gateway (192.168.3.1) 1.617 ms 1.550 ms 1.531 ms ... 9 one.one.one.one (1.1.1.1) 17.265 ms 17.246 ms 18.600 ms
Окей, через домашній роутер.
І до якогось серверу в AWS VPC:
$ traceroute 10.0.42.95 traceroute to 10.0.42.95 (10.0.42.95), 30 hops max, 60 byte packets 1 172.16.0.1 (172.16.0.1) 124.407 ms 124.410 ms 124.417 ms ...
Через VPN.
І навіть SSH до інстансів в приватній мережі працює:
$ ssh -i test-to-del.pem [email protected] ... ubuntu@ip-10-0-45-127:~$
Чудово.
Linux Systemd та Pritunl/OpenVPN autostart
Давайте зробимо, аби конект був постійно.
Створюємо каталог:
$ sudo mkdir /etc/pritunl-client
Переносимо конфіг:
$ sudo mv org-all_test-user_org-all-serv.ovpn /etc/pritunl-client/work.ovpn
Пишемо самий простий файл /etc/systemd/system/pritunl-org.service
:
[Unit] Description=Pritunl Work [Service] Restart=always WorkingDirectory=/etc/pritunl-client/ ExecStart=/usr/bin/openvpn --config work.ovpn ExecStop=killall openvpn [Install] WantedBy=multi-user.target
І перевіряємо:
$ systemctl start pritunl-org.service ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ==== Authentication is required to start 'pritunl-org.service'.
Ще раз роути:
$ route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.3.1 0.0.0.0 UG 100 0 0 enp2s0f0 0.0.0.0 192.168.3.1 0.0.0.0 UG 600 0 0 wlan0 0.0.0.0 192.168.3.1 0.0.0.0 UG 1002 0 0 enp2s0f0 10.0.0.0 172.16.0.1 255.255.0.0 UG 0 0 0 tun0
Все є.
Додаємо в автостарт:
$ systemctl enable pritunl-org.service ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-unit-files ==== Authentication is required to manage system service or unit files. Authenticating as: root Password: ==== AUTHENTICATION COMPLETE ==== Created symlink /etc/systemd/system/multi-user.target.wants/pritunl-org.service -> /etc/systemd/system/pritunl-org.service.
Готово.