---
tags: [network, security, tooling]
verified: 2026-09-02
---

# Обратный прокси и TLS: nginx vs Caddy

## Обзор

Типичный самостоятельно поднятый сервис слушает на `127.0.0.1` и каком-нибудь произвольном порту: Sonarr на 8989, Radarr на 7878, Prowlarr на 9696, Plex на 32400 (см. [медиасервер](./media-server-guide.md)). Пока к нему ходят с той же машины, этого достаточно. Как только сервис нужно открыть наружу, выясняется, что порт — плохая единица публикации: его нужно помнить, его нельзя защитить сертификатом, выданным на имя, и открывать десяток портов в firewall никто не хочет.

Обратный прокси решает эту задачу подменой единицы адресации: наружу смотрит один порт 443, а разбор идёт по имени хоста из TLS SNI и заголовка `Host`. Всё, что за прокси, остаётся на loopback и в firewall не появляется вовсе. Побочно прокси становится местом, где централизованы TLS, логи доступа, лимиты и заголовки — то есть всё, что не хочется реализовывать в каждом сервисе отдельно.

Практический выбор при старте сводится к двум серверам: nginx и Caddy. Разница между ними не столько в возможностях, сколько в том, что каждый считает работой администратора, а что — своей. Caddy получает и продлевает сертификаты сам, без внешних инструментов, и его конфиг для типовой задачи занимает три строки. nginx традиционно перекладывает TLS на отдельный ACME-клиент, зато даёт заметно более тонкий контроль над всем остальным. Начиная с версии 1.29.0 у nginx появился собственный ACME-модуль, и этот водораздел стал менее резким — но, как обычно, с оговорками.

## Что именно делает обратный прокси

Терминация TLS — главная функция. Клиент устанавливает TLS-соединение с прокси, прокси расшифровывает трафик и передаёт его дальше по HTTP, обычно по loopback. Сертификат нужен только прокси; ни один из проксируемых сервисов о TLS не знает и знать не должен. Это же объясняет, почему `X-Forwarded-Proto` приходится проставлять руками: приложение видит обычный HTTP и без подсказки будет считать, что работает по незащищённому каналу.

Маршрутизация по имени идёт в два этапа, и их полезно различать. Сначала клиент в TLS-хендшейке передаёт SNI — по нему прокси выбирает, каким сертификатом отвечать. Только после расшифровки становится виден заголовок `Host`, по которому выбирается upstream. Отсюда следствие: сертификат должен существовать до того, как прокси увидит содержимое запроса, и «выдать сертификат по факту первого обращения» — это отдельный механизм (у Caddy — on-demand TLS), а не поведение по умолчанию.

Буферизация и таймауты — то место, где прокси перестаёт быть прозрачным. По умолчанию nginx накапливает ответ upstream в память и на диск, прежде чем отдавать клиенту. Для API это разумно: медленный клиент не держит воркер приложения. Для потокового видео или server-sent events это ломает всё — ответ никогда не заканчивается, буфер растёт, клиент ждёт. Поэтому Plex, стримы и SSE требуют явного отключения буферизации.

WebSocket проходит через прокси только при явном сотрудничестве. Протокол начинается с обычного HTTP-запроса с заголовками `Upgrade: websocket` и `Connection: Upgrade`, после чего соединение перестаёт быть HTTP. `Connection` — hop-by-hop заголовок, то есть прокси обязан его не пересылать, и без специальной настройки апгрейд молча не происходит. Внешне это выглядит как «интерфейс загрузился, но ничего не обновляется».

## DNS: предварительное условие

Пока имя не резолвится в адрес прокси, ни сертификат, ни маршрутизация не заработают: HTTP-01 требует, чтобы удостоверяющий центр достучался до вас по этому имени.

Для сервера с постоянным адресом достаточно `A`-записи (и `AAAA`, если есть IPv6). Отдельные сервисы удобно вешать не отдельными записями, а `CNAME` на основное имя или wildcard-записью `*.example.com` — тогда добавление нового сервиса не требует правки DNS вообще.

TTL стоит понизить заранее, за сутки до планируемого переезда, а не в момент переезда: старое значение уже разошлось по резолверам и будет жить положенное ему время. Для домашнего сервера с меняющимся адресом нужен dynamic DNS — либо у регистратора с API, либо через сервис вроде duckdns; практическая часть здесь тривиальна, важно лишь помнить, что TTL таких записей должен быть минимальным.

Проверять стоит не тем резолвером, который настроен локально, а авторитетным сервером зоны — иначе легко полчаса смотреть на закешированный старый ответ:

```bash
dig +short sonarr.example.com
dig +short @1.1.1.1 sonarr.example.com          # мимо локального кеша
dig +trace sonarr.example.com | tail -5         # цепочка делегирования целиком
```

## ACME: как появляется сертификат

ACME — протокол, которым клиент доказывает удостоверяющему центру контроль над именем и получает сертификат. Способов доказательства три, и выбор между ними определяет почти всю остальную конфигурацию.

HTTP-01 — центр обращается к `http://<имя>/.well-known/acme-challenge/<token>` и сверяет содержимое. Просто и не требует ничего, кроме доступного снаружи порта 80. Именно поэтому порт 80 нельзя закрывать «за ненадобностью»: без него продление сломается, причём молча и через два месяца после настройки.

DNS-01 — клиент создаёт TXT-запись `_acme-challenge.<имя>`. Требует API у DNS-провайдера, зато не требует вообще никакой доступности сервера снаружи. Это единственный способ получить wildcard-сертификат и единственный работающий вариант для сервиса, который наружу не смотрит (внутренний хост, сервер за NAT без проброса).

TLS-ALPN-01 — доказательство внутри самого TLS-хендшейка на порту 443, через отдельный ALPN-протокол. Полезен, когда 80-й порт занят или закрыт, но wildcard через него получить нельзя.

### Сроки жизни и профили

Let's Encrypt переводит выдачу на короткие сертификаты поэтапно, через механизм ACME-профилей. По состоянию на август 2026 картина такая:

| Профиль | Срок | Примечание |
|---|---|---|
| `classic` | 90 дней | используется по умолчанию, если профиль не запрошен явно |
| `tlsserver` | 45 дней | переведён на 45 дней 13 мая 2026 |
| `shortlived` | ~6 дней | то же, что `tlsserver`, но с недельным сроком |

Профиль `classic` перейдёт на 64 дня 10 февраля 2027 и на 45 дней 16 февраля 2028; параллельно сокращается срок переиспользования авторизации — с нынешних 30 дней до 7 часов к 2028 году. Практический вывод один и он не нов, но с каждым годом жёстче: продление обязано быть автоматическим и обязано мониториться. Схема «получил сертификат руками, поставил напоминание в календарь» перестаёт работать физически.

Лимиты выдачи считаются по registered domain и рассчитаны на то, что вы не упрётесь в них при нормальной эксплуатации; продления из-под лимита на новые имена выведены. Упираются в лимиты обычно при отладке — когда конфиг не работает и хочется перевыпустить сертификат ещё раз. Для этого есть staging-окружение: сертификаты из него невалидны для браузера, но лимиты там несопоставимо выше. Отлаживать нужно на staging, переключаясь на боевой ACME-эндпоинт только когда всё сошлось.

## nginx vs Caddy

Разница между ними — это разница в ответе на вопрос «кто отвечает за сертификат».

Caddy считает, что HTTPS — не фича, а состояние по умолчанию. Он сам определяет, для каких имён нужны сертификаты, сам получает их при первом запуске, сам продлевает, сам держит редирект с 80 на 443 и сам выбирает разумные параметры TLS. Конфиг для типового случая — имя и адрес upstream, больше ничего. Пока задача типовая, писать нечего; ошибиться тоже почти негде.

nginx исходит из того, что решения принимает администратор. Он не будет получать сертификат, пока ему не скажут, и не поставит редирект, пока его не напишут. Взамен он даёт то, чего у Caddy нет в сопоставимом виде: кеширование, тонкое управление буферами и таймаутами, лимиты запросов и соединений, `stream`-модуль для проксирования произвольного TCP/UDP, огромный корпус готовых рецептов под любой софт и предсказуемое поведение под нагрузкой, изученное за два десятилетия.

Собственный ACME-модуль сблизил их, но не уравнял. Модуль `ngx_http_acme_module` написан на Rust, поставляется как динамический модуль (готовые пакеты — начиная с nginx 1.29.0) и умеет HTTP-01, а с версии модуля 0.2.0 — и TLS-ALPN-01. DNS-01 он не поддерживает, а значит wildcard-сертификаты по-прежнему требуют внешнего клиента вроде certbot, lego или acme.sh. Кроме того, модуль есть далеко не в каждом дистрибутивном пакете nginx, и это стоит проверить до того, как строить на нём конфигурацию.

| | nginx | Caddy |
|---|---|---|
| TLS из коробки | нет; ACME-модуль с 1.29.0 либо внешний клиент | да, включая редирект с 80 |
| DNS-01 / wildcard | только внешним клиентом | да, но провайдер вкомпилируется в бинарник |
| Объём конфига для типовой задачи | ~20 строк | ~3 строки |
| HTTP/3 | с 1.25.0, включается явно, помечен экспериментальным | включён по умолчанию |
| Кеширование, лимиты, TCP/UDP-проксирование | да | ограниченно |
| Доверие к `X-Forwarded-*` | нужно настроить самому | по умолчанию игнорируется |
| Готовые рецепты под конкретный софт | практически под всё | заметно меньше |
| Модули | динамические, подключаются в рантайме | вкомпилируются при сборке |

Выбор по ситуации выходит довольно чёткий. Для домашнего сервера, VPS с несколькими сервисами и вообще для всего, где хочется настроить один раз и забыть, — Caddy: он закрывает 90 % задач тремя строками и не даёт забыть про продление. Для случаев, где нужен кеш, тонкие лимиты, проксирование не-HTTP или где уже есть работающая nginx-инфраструктура и штат рецептов, — nginx.

Отдельно стоит отметить асимметрию в расширяемости, которая всплывает не сразу. У nginx модули динамические: поставил пакет, добавил `load_module`, перезапустил. У Caddy DNS-провайдеры и прочие плагины вкомпилируются в бинарник — через `xcaddy build --with ...` либо командой `caddy add-package`, которая пересобирает бинарник на месте. Для одиночного сервера это разовое неудобство, но оно ломает привычную схему «обновляю пакеты пакетным менеджером»: собранный вручную Caddy обновляется отдельно.

Существенная поправка: то, что написано выше про возможности, относится к актуальным версиям upstream, а в дистрибутивах лежит не они. Разрыв здесь больше обычного. Встроенный ACME-модуль появился в nginx 1.29.0 — в стабильных дистрибутивах с замороженными версиями (Debian 13 — nginx 1.26.3, RHEL-семейство — как правило ещё старше) его просто нет, тогда как в rolling-дистрибутивах он уже доступен. С Caddy та же история наоборот: пакет может быть на несколько минорных версий позади (в Debian 13 — 2.6.2 при текущей ветке 2.11), и, что важнее, DNS-провайдеры в чужой бинарник не добавить.

Практический вывод один: прежде чем строить конфигурацию на встроенном ACME-модуле или на DNS-01 в Caddy, нужно проверить, что именно стоит в системе. Если версия не дотягивает, вариантов два — классическая связка nginx + certbot, которая работает на любой версии, либо upstream-репозиторий проекта. Второй путь всегда имеет цену: пакет выпадает из-под security-обновлений дистрибутива, и следить за ним теперь ваша забота.

## Практика: один сервис, два сервера

Задача одна и та же — опубликовать Sonarr с `127.0.0.1:8989` под именем `sonarr.example.com`. Конфиги ниже от дистрибутива не зависят; различаются имена пакетов, пути и способ управления сервисом.

### Установка

Дистрибутивный путь — всё из штатного пакетного менеджера, обновляется вместе с системой:

| Дистрибутив | nginx + ACME-клиент | Caddy |
|---|---|---|
| Debian/Ubuntu | `apt install nginx certbot python3-certbot-nginx` | `apt install caddy` |
| RHEL/Fedora | `dnf install nginx certbot python3-certbot-nginx` | `dnf install caddy` |
| Arch | `pacman -S nginx certbot certbot-nginx` | `pacman -S caddy` |

Ставить оба сервера сразу не нужно и вредно: они подерутся за 80 и 443.

Если версия в дистрибутиве не дотягивает до нужной возможности — встроенного ACME-модуля nginx, актуального HTTP/3, DNS-провайдеров Caddy — подключается репозиторий upstream. У nginx это [официальный репозиторий nginx.org](https://nginx.org/en/linux_packages.html) с отдельным пакетом `nginx-module-acme`, у Caddy — [репозиторий проекта на Cloudsmith](https://caddyserver.com/docs/install). Проверить, что реально стоит:

```bash
nginx -v
caddy version
```

Отдельный случай — Caddy с DNS-провайдером для wildcard: его не ставят, а собирают, потому что провайдеры вкомпилируются в бинарник (см. ниже).

### Caddy

```conf
# /etc/caddy/Caddyfile
sonarr.example.com {
    reverse_proxy 127.0.0.1:8989
}
```

Это полный конфиг. Caddy получит сертификат при старте, поднимет редирект с 80 на 443, проставит `X-Forwarded-For`, `X-Forwarded-Proto` и `X-Forwarded-Host` (остальные заголовки, включая `Host`, уходят к upstream как есть), и будет продлевать сертификат сам. Остальные сервисы добавляются такими же блоками:

```conf
radarr.example.com {
    reverse_proxy 127.0.0.1:7878
}

plex.example.com {
    reverse_proxy 127.0.0.1:32400 {
        flush_interval -1          # отключить буферизацию: стриминг
    }
}
```

Перечитать конфиг без разрыва соединений:

```bash
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
```

### nginx с внешним ACME-клиентом

Классический вариант — сертификат получает certbot, nginx только читает файлы:

```bash
certbot certonly --nginx -d sonarr.example.com
certbot renew --dry-run                   # проверить, что продление реально работает
systemctl list-timers 'certbot*'          # убедиться, что таймер существует и активен
```

Конфиг сайта:

```conf
# /etc/nginx/conf.d/upgrade-map.conf — один раз на весь сервер
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
```

```conf
# /etc/nginx/conf.d/sonarr.conf
server {
    listen 80;
    server_name sonarr.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;                             # с 1.25.1 вместо параметра listen ... http2
    server_name sonarr.example.com;

    ssl_certificate     /etc/letsencrypt/live/sonarr.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/sonarr.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8989;

        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_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}
```

Двадцать строк против трёх — и каждая из них может быть забыта. Именно этот разрыв Caddy и закрывает.

```bash
nginx -t && systemctl reload nginx
```

### nginx со встроенным ACME-модулем

Готовые пакеты `nginx-module-acme` выпускаются начиная с nginx 1.29.0 (из исходников модуль собирается и против более старых веток, от 1.22.0), поэтому в дистрибутивах с замороженными версиями его обычно нет — там он появляется вместе с репозиторием nginx.org. Если версия подходит, внешний клиент не нужен вовсе:

```conf
load_module modules/ngx_http_acme_module.so;

http {
    resolver 127.0.0.1:53;                # модуль требует резолвер

    acme_issuer letsencrypt {
        uri                     https://acme-v02.api.letsencrypt.org/directory;
        state_path              /var/lib/nginx/acme-letsencrypt;
        accept_terms_of_service;
    }

    acme_shared_zone zone=acme_shared:1M;

    server {
        listen 443 ssl;
        server_name sonarr.example.com;

        acme_certificate      letsencrypt;
        ssl_certificate       $acme_certificate;
        ssl_certificate_key   $acme_certificate_key;
        ssl_certificate_cache max=2;

        location / {
            proxy_pass http://127.0.0.1:8989;
        }
    }

    server {
        listen 80;                        # обязателен: сюда приходит HTTP-01
        server_name sonarr.example.com;
        return 404;
    }
}
```

Запросы к `/.well-known/acme-challenge/` модуль перехватывает сам, поэтому явного `location` для них не нужно — но сервер на 80-м порту существовать обязан. Каталог `state_path` хранит ключ аккаунта и выданные сертификаты; его нужно бэкапить наравне с конфигом и не давать читать кому попало.

### Wildcard через DNS-01

Wildcard снимает необходимость трогать конфиг прокси и DNS при добавлении сервиса. У Caddy для этого нужен вкомпилированный DNS-провайдер:

```bash
xcaddy build --with github.com/caddy-dns/cloudflare
```

```conf
*.example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }

    @sonarr host sonarr.example.com
    handle @sonarr {
        reverse_proxy 127.0.0.1:8989
    }

    @radarr host radarr.example.com
    handle @radarr {
        reverse_proxy 127.0.0.1:7878
    }
}
```

У nginx — внешним клиентом, поскольку встроенный модуль DNS-01 не умеет:

```bash
certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d 'example.com' -d '*.example.com'
```

Токен DNS-провайдера — полноценный секрет: он позволяет править зону, а значит выпускать сертификаты на любое имя в ней. Права на файл `0600`, скоуп токена — по возможности только на нужную зону и только на запись TXT.

### Firewall и автозапуск

Автозапуск — обычный systemd:

```bash
systemctl enable --now caddy        # либо nginx
systemctl status caddy
```

Оба сервера умеют перечитывать конфиг без разрыва соединений, и пользоваться нужно именно этим, а не `restart`: перезапуск рвёт активные сессии и на медиасервере это заметно сразу.

В firewall открываются ровно два порта. Порт 80 нужен не для сайта, а для редиректа и для HTTP-01 — закрывать его нельзя. На уровне nftables это одно правило:

```bash
nft add rule inet filter input tcp dport { 80, 443 } accept
```

Если поверх nftables стоит обёртка — ufw или firewalld — правило добавляется через неё, иначе оно потеряется при следующей перезагрузке правил:

```bash
ufw allow 80/tcp
ufw allow 443/tcp
```

Сами сервисы при этом наружу не открываются вовсе — они остаются на `127.0.0.1`, и это стоит проверить явно, потому что многие из них по умолчанию слушают `0.0.0.0`:

```bash
ss -tlnp | grep -E ':(8989|7878|9696|32400)'
```

Если в выводе `0.0.0.0:8989`, а не `127.0.0.1:8989` — сервис доступен в обход прокси, и firewall остаётся единственным, что его прикрывает. Правильное лечение — заставить сервис слушать loopback, а не полагаться на правила.

## Заголовки проксирования

После терминации TLS приложение видит соединение от `127.0.0.1` по обычному HTTP. Всё, что оно знает о настоящем клиенте, приходит в заголовках, которые проставил прокси: `X-Forwarded-For` — цепочка адресов, `X-Forwarded-Proto` — исходная схема, `Host` — запрошенное имя.

Отсюда два самых частых симптома. Если приложение генерирует ссылки с `http://` вместо `https://` или уходит в редирект-петлю — не проставлен `X-Forwarded-Proto`. Если в логах приложения у всех клиентов один и тот же адрес — либо не проставлен `X-Forwarded-For`, либо приложение ему не доверяет.

Доверие здесь — не формальность. Заголовок `X-Forwarded-For` присылает клиент, и подделать его тривиально. Если прокси или приложение принимают его без разбора, любой желающий подменит свой адрес в логах и обойдёт ограничения по IP. Правило простое: доверять `X-Forwarded-*` можно только от адресов, которые вы сами контролируете.

В nginx доверие настраивается явно и по умолчанию отсутствует:

```conf
set_real_ip_from  10.100.0.0/24;      # только адреса своего туннеля
real_ip_header    X-Forwarded-For;
real_ip_recursive on;
```

Caddy ведёт себя строже из коробки: входящие `X-Forwarded-*` он по умолчанию игнорирует именно для защиты от подделки. Список доверенных источников задаётся глобально, чтобы применяться ко всем обработчикам сразу:

```conf
{
    servers {
        trusted_proxies static 10.100.0.0/24
    }
}
```

Отдельный случай — когда перед вашим прокси стоит ещё один: CDN или VPS-точка входа. Тогда `X-Forwarded-For` приходит уже непустым, и настоящий адрес клиента находится не в конце цепочки, а по позиции, которая зависит от числа доверенных прокси. Считать «последний элемент» в такой схеме неверно.

## Нюансы и грабли

Редирект-петля — почти всегда следствие того, что приложение настроено само редиректить на HTTPS, но `X-Forwarded-Proto` до него не доходит. Приложение видит HTTP, отправляет редирект на HTTPS, прокси снова приходит к нему по HTTP, и так до упора. Лечится заголовком, а не отключением редиректа в приложении.

WebSocket, не проходящий через прокси, диагностируется по симптому «страница открылась, данные не обновляются». В nginx нужны `proxy_http_version 1.1` и пара `Upgrade`/`Connection` через map — без map конструкция ломает обычные запросы, потому что `Connection: upgrade` будет проставляться всегда. Начиная с 1.29.7 первая половина рецепта отпадает сама: nginx по умолчанию ходит к upstream по HTTP/1.1 и держит keepalive-кеш соединений, так что явный `proxy_http_version 1.1` нужен только на старых ветках. Caddy делает это самостоятельно.

Буферизация бьёт по двум сценариям. Стриминг и SSE требуют её отключения в nginx (`proxy_buffering off`), иначе ответ не доходит до клиента; в Caddy отключать нечего — периодического сброса он не делает по умолчанию вовсе, а ответы с `Content-Type: text/event-stream` и ответы неизвестной длины сбрасывает клиенту немедленно, игнорируя `flush_interval`. Директива `flush_interval -1` там нужна только для потоков, которые под эти условия не подпадают. Большие загрузки упираются в `client_max_body_size`, который по умолчанию равен одному мегабайту и отдаёт `413` — на медиасервере это всплывает сразу.

HSTS стоит включать осознанно. Заголовок `Strict-Transport-Security` говорит браузеру ходить на это имя только по HTTPS, и он кешируется на указанный срок. Если что-то пойдёт не так с сертификатом, откатиться на HTTP уже не получится — браузер не пустит. На поддомене, где вы экспериментируете, это лишний риск; `preload` без понимания последствий не включать вовсе.

Порт 80 нельзя закрывать при HTTP-01. Соблазн возникает после того, как всё заработало по HTTPS: 80-й вроде бы нужен только для редиректа. Но через него же проходит продление, и поломка обнаружится через два месяца, когда сертификат истечёт.

Мониторинг срока действия — единственная защита от тихого отказа автопродления. Автоматика ломается по причинам, никак не связанным с прокси: сменился API-токен DNS, провайдер закрыл порт, кончилось место на диске. Рассчитывать на письмо-напоминание больше нельзя: Let's Encrypt выключил рассылку уведомлений об истечении ещё 4 июня 2025 года и удалил адреса из базы ACME-аккаунтов. Простейшая проверка снаружи:

```bash
echo | openssl s_client -connect sonarr.example.com:443 -servername sonarr.example.com 2>/dev/null \
  | openssl x509 -noout -dates -subject
```

С переходом на 45-дневные сертификаты запас времени на реакцию сокращается вдвое, поэтому проверку разумно повесить на регулярный запуск и завести алерт.

HTTP/3 включается по-разному. Caddy поднимает его по умолчанию. nginx поддерживает QUIC и HTTP/3 начиная с 1.25.0; в официальных бинарных пакетах модуль собран, при сборке из исходников нужен `--with-http_v3_module`, а в конфиге — явный `listen 443 quic` рядом с обычным `listen 443 ssl`. Документация nginx до сих пор помечает поддержку экспериментальной, а для полноценного QUIC нужен OpenSSL 3.5.1 и новее: с более старой версией собирается слой совместимости, не умеющий early data.

## Сравнение с FreeBSD

Принципиальной разницы в самой схеме нет: и nginx, и Caddy есть в портах и пакетах, конфиги идентичны. Отличается обвязка вокруг.

Сервисы включаются через `rc.conf`, а не systemd (см. [rc-систему](./freebsd-rc-system.md)):

```sh
sysrc nginx_enable=YES
service nginx reload
```

Конфиги лежат в `/usr/local/etc/nginx/` и `/usr/local/etc/caddy/` — по общей логике размещения стороннего софта в `/usr/local`, разобранной в статье про [файловую структуру](./freebsd-filesystem-hierarchy.md).

Firewall — PF вместо nftables, и правила для 80/443 пишутся в `/etc/pf.conf` (см. [сеть FreeBSD](./freebsd-networking.md)):

```conf
pass in on $ext_if proto tcp to port { 80, 443 }
```

Из ACME-клиентов на FreeBSD популярнее `acme.sh` и `dehydrated` — оба есть в портах, оба обходятся без Python-обвязки certbot, что на FreeBSD ощутимее, чем на Linux. Продление вешается на `cron` или `periodic`, а не на systemd-таймер.

Если прокси и сервисы разнесены по jail'ам, схема получается особенно опрятной: прокси в одном jail со своим адресом, сервисы в других, между ними — внутренняя сеть, наружу торчит только прокси. Подробности изоляции — в статье про [jails](./freebsd-jails.md).

## Источники

- [ACME (RFC 8555)](https://datatracker.ietf.org/doc/html/rfc8555) — сам протокол
- [Let's Encrypt: Certificate Profiles](https://letsencrypt.org/docs/profiles/) — профили `classic`, `tlsserver`, `shortlived` и их сроки
- [Let's Encrypt: Decreasing Certificate Lifetimes to 45 Days](https://letsencrypt.org/2025/12/02/from-90-to-45) — график перехода на короткие сертификаты
- [Let's Encrypt: Shorter Certificate Lifetimes and Rate Limits](https://letsencrypt.org/2026/02/24/rate-limits-45-day-certs) — влияние на лимиты выдачи
- [Let's Encrypt: Rate Limits](https://letsencrypt.org/docs/rate-limits/) — действующие лимиты
- [Let's Encrypt: Ending Expiration Emails](https://letsencrypt.org/2025/01/22/ending-expiration-emails/) — почему письма об истечении больше не приходят
- [Let's Encrypt: Staging Environment](https://letsencrypt.org/docs/staging-environment/) — окружение для отладки
- [nginx: ngx_http_acme_module](https://nginx.org/en/docs/http/ngx_http_acme_module.html) — встроенный ACME-модуль
- [nginx/nginx-acme](https://github.com/nginx/nginx-acme) — исходники модуля, поддерживаемые типы challenge
- [nginx: Support for QUIC and HTTP/3](https://nginx.org/en/docs/quic.html) — сборка и включение HTTP/3
- [nginx: ngx_http_proxy_module](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) — директивы проксирования
- [nginx: ngx_http_realip_module](https://nginx.org/en/docs/http/ngx_http_realip_module.html) — `set_real_ip_from` и доверие к заголовкам
- [Caddy: Automatic HTTPS](https://caddyserver.com/docs/automatic-https) — что и когда Caddy делает сам
- [Caddy: reverse_proxy](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy) — директива и её опции
- [Caddy: Global options](https://caddyserver.com/docs/caddyfile/options) — `trusted_proxies` и прочие глобальные настройки
- [xcaddy](https://github.com/caddyserver/xcaddy) — сборка Caddy с плагинами
- [caddy-dns](https://github.com/caddy-dns) — DNS-провайдеры для DNS-01
- [nginx: официальный репозиторий пакетов](https://nginx.org/en/linux_packages.html) — свежие версии и `nginx-module-acme`
- [Caddy: установка](https://caddyserver.com/docs/install) — пакеты и сборка из исходников
- [Repology: nginx](https://repology.org/project/nginx/versions) и [caddy](https://repology.org/project/caddy/versions) — какие версии в каком дистрибутиве
- [nftables wiki](https://wiki.nftables.org/) — правила фильтрации
- [Mozilla SSL Configuration Generator](https://ssl-config.mozilla.org/) — актуальные наборы шифров и параметров TLS

---

## Связанные заметки

- [Медиасервер: Transmission + Sonarr/Radarr + Plex](./media-server-guide.md) — сервисы, которые этот прокси публикует
- [Linux — сеть: стек, конфигурация, firewall](./linux-networking.md) — nftables и правила для 80/443
- [Linux — безопасность: hardening, SELinux/AppArmor](./linux-security-hardening.md) — публичный сервис в общей модели защиты
- [FreeBSD — сеть](./freebsd-networking.md) — PF вместо nftables, для сравнения
- [OpenVPN против WireGuard](./openvpn-vs-wireguard.md) — SNI и маскировка трафика с другой стороны, со стороны VPN
- [Самохостинг за NAT: VPS как точка входа](./selfhosting-behind-nat.md) — как трафик доходит до прокси, если белого адреса нет; где терминировать TLS
- [Резервное копирование: стратегия и проверка восстановления](./backup-strategy.md) — состояние ACME и ключи учётной записи как обязательный объект бэкапа
- [Linux — время и синхронизация](./linux-time-sync.md) — почему уехавшие часы выглядят как недействительный сертификат
- [Наблюдаемость: Prometheus, node_exporter, Grafana, Loki](./observability-stack.md) — алерт на сломавшееся автопродление и публикация Grafana через прокси
- [Debian: модель релизов и управление пакетами](./debian-releases-and-packages.md) — почему версия в стабильном дистрибутиве меняет рекомендацию, а не только команду
- [Эксплуатация локальных ИИ-сервисов](./ai-services-operations.md) — таймауты под долгую генерацию и буферизация потокового ответа модели
- [AEAD: аутентифицированное шифрование, nonce и лимиты](./crypto-aead.md) — что именно обещают AEAD-шифры, которыми терминируется TLS, и откуда берётся требование перевыработки ключей
- [Обмен ключами: X25519, KEM, вывод ключей и привязка к транскрипту](./crypto-key-exchange.md) — что происходит в рукопожатии до терминации TLS: за что ручается сертификат и откуда берутся ключи записи
- [AES-GCM против ChaCha20-Poly1305: как выбирать](./crypto-aes-vs-chacha20.md) — почему серверу не стоит навязывать AES-GCM и зачем нужен `ssl_conf_command Options PrioritizeChaCha`
