Є сервер з Ubuntu, на якому запущений один з наших сервісів. Сам сервіс ще в стадії Proof of Concept, тому в Kubernetes не переносимо – але ганяти трафік plain-text, тим більш коли це запити до AI Agents з реквестами та відповідями – ідея так собі.
Тому є задача додати NGINX як проксі до сервісів в Docker та налаштувати TLS/SSL.
Для отримання сертифіката використаємо Let’s Encrypt, а аби не морочитись кожні три місяці (чи вже місяць?) з renew – додамо автоматизацію апдейтів через AWS Route 53, бо сервер живе в AWS і домен ми тримаємо там жеж.
Сама схема з DNS validation через AWS Route 53 дуже стара, колись про неї писав в пості OpenVPN: Let’s Encrypt DNS verification с certbot и AWS Route53 и обновление сертификата в OpenVPN Access Server, але то було у 2019 році, і там не було NGINX.
A short note: взагалі, правильно казати TLS, а не SSL – бо остання версія SSL 3.0 була опублікована ще у 1996 році, а TLS – сучасна його заміна з актуальною на сьогодні версією 1.3 від 2018 року – див. RFC 8446. Але я більше звик до виразу “SSL”, тому буду казати або SSL, або TLS/SSL.
Зміст
Установка NGINX на Ubuntu
Власне, маємо AWS EC2 з Ubuntu 24:
# cat /etc/os-release PRETTY_NAME="Ubuntu 24.04.4 LTS" NAME="Ubuntu" VERSION_ID="24.04" VERSION="24.04.4 LTS (Noble Numbat)"
Тут все стандартно – оновлюємо списки пакетів, з apt встановлюємо NGINX, запускаємо сервіс і додаємо в автостарт:
# apt update # apt install -y nginx # systemctl enable --now nginx
Перевіряємо локально, що все працює:
# curl -I http://127.0.0.1 HTTP/1.1 200 OK Server: nginx/1.24.0 (Ubuntu)
Перевіряємо і при потребі відкриваємо порти 80 та 443 на AWS Security Group цього EC2:
Перевіряємо вже по домену і публічному IP:
# curl -I --connect-timeout 10 http://morpheus-agent.ops.example.co HTTP/1.1 200 OK Server: nginx/1.24.0 (Ubuntu) ...
Установка certbot для Let’s Encrypt
certbot – ACME-клієнт (ACME – Automatic Certificate Management Environment) для роботи з сертифікатами, який серед інших підтримує Let’s Encrypt Certificate Authority.
До самого certbot є багато плагінів, нам для роботи з AWS Route53 треба python3-certbot-dns-route53, і для NGINX – стандартний python3-certbot-nginx.
Встановлюємо пакети:
# apt install -y certbot python3-certbot-nginx python3-certbot-dns-route53
Перевіряємо сам клієнт:
# certbot --version certbot 2.9.0
І плагіни:
# certbot plugins Saving debug log to /var/log/letsencrypt/letsencrypt.log - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * dns-route53 Description: Obtain certificates using a DNS TXT record (if you are using AWS Route53 for DNS). ... * nginx Description: Nginx Web Server plugin ...
Налаштування AWS IAM
Аби наш клієнт мав змогу вносити validation records в AWS Route 53 hosted DNS zone – йому, вочевидь, треба мати на це права.
Перевіряємо AWS EC2 Instance Profile зараз:
# aws --version
aws-cli/2.35.21 Python/3.14.6 Linux/6.17.0-1013-aws exe/x86_64.ubuntu.24
# aws sts get-caller-identity
{
"UserId": "ARO***HHT:i-02569635c6284bb35",
"Account": "492***148",
"Arn": "arn:aws:sts::492***148:assumed-role/SSMInstanceProfile/i-02569635c6284bb35"
}
SSMInstanceProfile – дефолтна AWS IAM Role для AWS Systems Manager, яка, очікувано, не дає доступу до AWS Route 53:
# aws route53 list-hosted-zones-by-name \ --query "HostedZones[?Name=='ops.example.co.' || Name=='example.co.'].[Name,Id]" \ --output table aws: [ERROR]: An error occurred (AccessDenied) when calling the ListHostedZonesByName operation: User: arn:aws:sts::492***148:assumed-role/SSMInstanceProfile/i-02569635c6284bb35 is not authorized to perform: route53:ListHostedZonesByName because no identity-based policy allows the route53:ListHostedZonesByName action
Тому створимо власну IAM Role із доступом до SSM та окремою DNS Zone.
Створення AWS IAM Role
Переходимо в AWS IAM > IAM Roles, додаємо нову роль.
В Trusted entity залишаємо дефолтне значення AWS service, в Use case – теж нічого не міняємо, залишаємо EC2:
Аби SSM продовжував працювати, відразу додаємо AWS Managed IAM Policy AmazonSSMManagedInstanceCore – а потім додамо власну для AWS Route 53:
Зберігаємо:
Створення IAM Policy для Route 53
Аби обмежити доступ окремою DNS-зоною – бо certbot виконує і додавання, і видалення записів, і не варто давати йому доступ всюди – в Route 53 знаходимо Hosted Zone ID:
Переходимо в IAM > Policies, створюємо нову.
Міняємо дефолтне значення:
На нашу власну політику, яка дозволяє читання всіх зон в Route 53, але обмежує права на редагування тільки конкретною <HOSTED_ZONE_ID>:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DiscoverHostedZone",
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListHostedZonesByName"
],
"Resource": "*"
},
{
"Sid": "ManageAcmeChallenge",
"Effect": "Allow",
"Action": "route53:ChangeResourceRecordSets",
"Resource": "arn:aws:route53:::hostedzone/<HOSTED_ZONE_ID>",
"Condition": {
"ForAllValues:StringEquals": {
"route53:ChangeResourceRecordSetsRecordTypes": [
"TXT"
],
"route53:ChangeResourceRecordSetsActions": [
"CREATE",
"UPSERT",
"DELETE"
]
}
}
},
{
"Sid": "CheckDnsChange",
"Effect": "Allow",
"Action": "route53:GetChange",
"Resource": "arn:aws:route53:::change/*"
}
]
}
Зберігаємо політику як, наприклад, CertbotRoute53DnsChallenge, повертаємо до IAM Role, яку створили вище, і через Attach Policies додаємо IAM Policy:
Переходимо до EC2, в Actions > Security > Modify IAM Role:
Підключаємо нову роль:
Перевіряємо доступ до Route 53 тепер:
# aws route53 list-hosted-zones-by-name --query "HostedZones[?Name=='ops.example.co.'].[Name,Id]"
[
[
"ops.example.co.",
"/hostedzone/Z02***OYY"
]
]
І тепер ми готові до налаштування NGINX.
Отримання TLS сертифікату з certbot
Отримуємо сертифікат, вказуємо опцію --dns-route53 – certbot сам “сходить” в AWS і створить всі необхідні записи для валідації домену:
# certbot certonly \ --dns-route53 \ --domain morpheus-agent.ops.example.co \ --email [email protected] \ --agree-tos \ --no-eff-email Saving debug log to /var/log/letsencrypt/letsencrypt.log Account registered. Requesting a certificate for morpheus-agent.ops.example.co Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/morpheus-agent.ops.example.co/fullchain.pem Key is saved at: /etc/letsencrypt/live/morpheus-agent.ops.example.co/privkey.pem
Перевіряємо, що certbot renew буде працювати:
# certbot renew --dry-run Saving debug log to /var/log/letsencrypt/letsencrypt.log - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Processing /etc/letsencrypt/renewal/morpheus-agent.ops.example.co.conf - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Account registered. Simulating renewal of an existing certificate for morpheus-agent.ops.example.co - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Congratulations, all simulated renewals succeeded:
Налаштування NGINX Virtual Host
Наш сервіс запущений в Docker на порту 3001:
# docker ps --format 'table {{.Names}}\t{{.Ports}}'
NAMES PORTS
...
app-hub 0.0.0.0:3001->3000/tcp, [::]:3001->3000/tcp
...
Створюємо файл /etc/nginx/sites-available/morpheus-agent.ops.example.co.
Стандартно – для запитів на порт 80 виконуємо редірект на 443/SSL, задаємо шляхи до сертифікату, в proxy_pass задаємо порт 3001 до нашого сервісу в Docker:
server {
listen 80;
listen [::]:80;
server_name morpheus-agent.ops.example.co;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name morpheus-agent.ops.example.co;
ssl_certificate /etc/letsencrypt/live/morpheus-agent.ops.example.co/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/morpheus-agent.ops.example.co/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
access_log /var/log/nginx/morpheus-agent.access.log;
error_log /var/log/nginx/morpheus-agent.error.log;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
Завжди не любив цю схему в Ubuntu з ручними сімлінками, і зазвичай для себе роблю нормальну просто через файли конфігурації в /etc/nginx/conf.d – але тут нехай буде “дефолтний” варіант.
Створюємо symlink з /etc/nginx/sites-available/ на /etc/nginx/sites-enabled/:
# ln -s /etc/nginx/sites-available/morpheus-agent.ops.example.co \ /etc/nginx/sites-enabled/morpheus-agent.ops.example.co
Перевіряємо синтаксис конфігурації, перечитуємо конфіги:
# nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful # systemctl reload nginx
Перевіряємо вже з HTTPS:
# curl -I https://morpheus-agent.ops.example.co HTTP/1.1 404 Not Found
404 тут – нам ок, головне, що нема помилок SSL.
Ще можемо глянути з openssl – побачити більше інформації по нашому сертифікату:
# openssl s_client \
-connect morpheus-agent.ops.example.co:443 \
-servername morpheus-agent.ops.example.co \
</dev/null 2>/dev/null |
openssl x509 -noout \
-subject \
-issuer \
-dates \
-serial \
-fingerprint \
-sha256 \
-ext subjectAltName
subject=CN = morpheus-agent.ops.example.co
issuer=C = US, O = Let's Encrypt, CN = YE2
notBefore=Aug 11 13:32:07 2026 GMT
notAfter=Nov 9 13:32:06 2026 GMT
serial=06BA69E938D84355CCDF27689AA265DD9926
sha256 Fingerprint=11:E0:E0:D0:F6:A1:15:43:1B:A4:DE:DF:14:7A:23:63:AF:BA:71:1E:7C:CF:42:A1:CC:26:C9:44:BE:8C:F2:17
X509v3 Subject Alternative Name:
DNS:morpheus-agent.ops.example.co
Все працює, все готово.
![]()









