#!/bin/sh set -euo pipefail # #region docker.frontend.entrypoint [C:4] [TYPE Module] [SEMANTICS docker,nginx,entrypoint,certs,ssl,passphrase] # @BRIEF Entrypoint для nginx-контейнера — установка корп. CA-сертификатов в Alpine, # авто-выбор HTTP или SSL конфига, поддержка passphrase-защищённых ключей и PKCS#12. # @PRE /opt/certs смонтирован (может быть пуст). Системные пакеты: ca-certificates, openssl. # SSL_KEY_PASSPHRASE — опциональный пароль от приватного ключа. # @POST Если найдены server.crt + server.key — активирован SSL конфиг. # Если есть server.p12 — извлечены .crt и .key. # Если server.key зашифрован + есть SSL_KEY_PASSPHRASE — ключ расшифрован. # CA-сертификаты из /opt/certs установлены в системное хранилище Alpine. # @SIDE_EFFECT Модифицирует /usr/local/share/ca-certificates/; перезаписывает nginx конфиг. # Расшифровывает приватный ключ в /etc/nginx/ssl/ при наличии passphrase. # @LAYER Infrastructure # @RELATION DEPENDS_ON -> [docker/nginx.conf] # @RELATION DEPENDS_ON -> [docker/nginx.ssl.conf] # @INVARIANT Порт 80 всегда активен (HTTP или редирект на HTTPS). # @INVARIANT Если нет server.crt — используется HTTP-only конфиг. # @INVARIANT Зашифрованный ключ без SSL_KEY_PASSPHRASE — nginx не стартует (crash-early). # @DATA_CONTRACT Input: /opt/certs/{server.crt, server.key, server.p12?} + SSL_KEY_PASSPHRASE? -> # Output: /etc/nginx/ssl/{server.crt, server.key} (дешифрованный) # @RATIONALE Поддержка парольных ключей и PKCS#12 нужна для корпоративных PKI, # которые часто выдают контейнеры .p12 с passphrase вместо отдельных файлов. # @REJECTED Хранение пароля в файле на диске (/opt/certs/passphrase.txt) отвергнуто — # переменная окружения безопаснее (не персистится в volume) и следует тому же # паттерну, что и остальные секреты в проекте. # #endregion docker.frontend.entrypoint # ====================================================================== # install_ca_certificates — корпоративные CA в Alpine # ====================================================================== # #region docker.frontend.entrypoint.install_ca_certificates [C:2] [TYPE Function] # @BRIEF Устанавливает .crt файлы из /opt/certs в системное хранилище Alpine. # @PRE /opt/certs может быть пуст или не существовать. # @POST Сертификаты скопированы в /usr/local/share/ca-certificates/ и добавлены в trust. install_ca_certificates() { local certs_dir="${CERTS_PATH:-/opt/certs}" if [ ! -d "$certs_dir" ]; then echo "[entrypoint] CERTS_PATH=${certs_dir} не существует — пропускаем CA-сертификаты" return 0 fi local count=0 for cert in "$certs_dir"/*.crt "$certs_dir"/*.pem; do [ -f "$cert" ] || continue # Не копируем server.crt — он для nginx, не для CA case "$(basename "$cert")" in server.crt|server.key) continue ;; esac cp -v "$cert" /usr/local/share/ca-certificates/ 2>&1 | sed 's/^/[entrypoint] /' count=$((count + 1)) done if [ "$count" -gt 0 ]; then echo "[entrypoint] Обновляем CA-сертификаты Alpine..." update-ca-certificates 2>&1 | sed 's/^/[entrypoint] /' echo "[entrypoint] ✅ Установлено ${count} корпоративных CA-сертификатов" else echo "[entrypoint] Нет CA-сертификатов для установки" fi } # #endregion docker.frontend.entrypoint.install_ca_certificates # ====================================================================== # extract_p12_if_needed — конвертация PKCS#12 в CRT+KEY # ====================================================================== # #region docker.frontend.entrypoint.extract_p12 [C:3] [TYPE Function] [SEMANTICS pkcs12,p12,certificate,extraction] # @BRIEF Если в /opt/certs есть server.p12, извлекает server.crt + server.key через openssl pkcs12. # @PRE /opt/certs/server.p12 может существовать. SSL_KEY_PASSPHRASE — опциональный пароль. # @POST Если server.p12 найден — server.crt и server.key извлечены в /opt/certs/. # Существующие server.crt/server.key НЕ перезаписываются (.p12 обрабатывается только # если нет .crt или .key). # @SIDE_EFFECT Записывает server.crt и server.key в CERTS_PATH. # @LAYER Infrastructure # @RELATION DEPENDS_ON -> [openssl (system package)] # @EXAMPLE: # ./certs/server.p12 (PKCS#12 контейнер) # SSL_KEY_PASSPHRASE=my-passphrase # → ./certs/server.crt + ./certs/server.key извлечены extract_p12_if_needed() { local certs_dir="${CERTS_PATH:-/opt/certs}" local p12_file="${certs_dir}/server.p12" [ -f "$p12_file" ] || return 0 # Не перезаписываем, если .crt и .key уже есть (приоритет ручной установки) local ssl_cert="${certs_dir}/server.crt" local ssl_key="${certs_dir}/server.key" if [ -f "$ssl_cert" ] && [ -f "$ssl_key" ]; then echo "[entrypoint] server.crt + server.key уже существуют — пропускаем извлечение из .p12" return 0 fi echo "[entrypoint] 📦 Найден server.p12 — извлекаем сертификат и ключ..." local pass_opt="" if [ -n "${SSL_KEY_PASSPHRASE:-}" ]; then pass_opt="-passin env:SSL_KEY_PASSPHRASE" fi # Извлечь сертификат (без ключа) # shellcheck disable=SC2086 openssl pkcs12 -in "$p12_file" \ $pass_opt \ -clcerts -nokeys -out "$ssl_cert" 2>&1 | sed 's/^/[entrypoint] /' # Извлечь приватный ключ (без шифрования на выходе) # shellcheck disable=SC2086 openssl pkcs12 -in "$p12_file" \ $pass_opt \ -nocerts -nodes -out "$ssl_key" 2>&1 | sed 's/^/[entrypoint] /' # Проверить, что извлеклось if [ -f "$ssl_cert" ] && [ -f "$ssl_key" ]; then echo "[entrypoint] ✅ Сертификат и ключ извлечены из server.p12" else echo "[entrypoint] ⚠️ Ошибка извлечения из server.p12 — проверьте SSL_KEY_PASSPHRASE" return 1 fi } # #endregion docker.frontend.entrypoint.extract_p12 # ====================================================================== # resolve_ssl_passphrase — получение пароля для ключа # ====================================================================== # #region docker.frontend.entrypoint.resolve_passphrase [C:1] [TYPE Function] [SEMANTICS passphrase,ssl] # @BRIEF Возвращает пароль для SSL-ключа из SSL_KEY_PASSPHRASE (или пустую строку). # Резервирует место для будущего расширения (Fernet-дешифровка SSL_KEY_PASSPHRASE_ENCRYPTED). # @POST Переменная ssl_passphrase установлена (может быть пуста). # @LAYER Infrastructure # @RELATION DEPENDS_ON -> [SSL_KEY_PASSPHRASE (env)] resolve_ssl_passphrase() { local passphrase="${SSL_KEY_PASSPHRASE:-}" # Будущее расширение: Fernet-дешифровка SSL_KEY_PASSPHRASE_ENCRYPTED # через ENCRYPTION_KEY, если SSL_KEY_PASSPHRASE не задана. echo "$passphrase" } # #endregion docker.frontend.entrypoint.resolve_passphrase # ====================================================================== # decrypt_key_if_needed — расшифровка passphrase-защищённого ключа # ====================================================================== # #region docker.frontend.entrypoint.decrypt_key [C:3] [TYPE Function] [SEMANTICS decryption,rsa,ssl,passphrase] # @BRIEF Проверяет, зашифрован ли server.key, и если да — расшифровывает его. # @PRE ssl_dir — директория, куда скопирован server.key. ssl_passphrase — пароль (может быть пуст). # @POST Если ключ зашифрован и пароль верен — ключ расшифрован в ssl_dir. # Если зашифрован, но пароля нет — crash с ошибкой (nginx не сможет стартовать). # @SIDE_EFFECT Перезаписывает ssl_dir/server.key расшифрованной версией. # @LAYER Infrastructure # @RELATION DEPENDS_ON -> [openssl (system package)] # @RELATION CALLED_BY -> [docker.frontend.entrypoint.select_nginx_config] # @INVARIANT Если ключ зашифрован и SSL_KEY_PASSPHRASE пуст — entrypoint exit(1). # Это crash-early поведение: лучше явная ошибка при старте, чем nginx, который # молча не принимает HTTPS-соединения. # @RATIONALE Расшифровка происходит ПОСЛЕ копирования ключа в /etc/nginx/ssl/, # а не mount-директорию, потому что /opt/certs смонтирован read-only. # Расшифрованный ключ остаётся в tmpfs (/etc/nginx/ssl/) и не пишется на диск хоста. decrypt_key_if_needed() { local ssl_dir="/etc/nginx/ssl" local ssl_key="${ssl_dir}/server.key" local ssl_passphrase="$1" [ -f "$ssl_key" ] || return 0 # Проверка: ключ зашифрован? if openssl rsa -in "$ssl_key" -check -noout 2>/dev/null; then # Ключ незашифрован — ничего не делаем return 0 fi # Ключ зашифрован — нужен пароль if [ -z "$ssl_passphrase" ]; then echo "[entrypoint] ❌ Приватный ключ зашифрован, но SSL_KEY_PASSPHRASE не задана." echo "[entrypoint] Установите переменную окружения SSL_KEY_PASSPHRASE или используйте" echo "[entrypoint] незашифрованный ключ." return 1 fi echo "[entrypoint] 🔐 Приватный ключ зашифрован — расшифровываем..." # Расшифровать через openssl rsa if openssl rsa -in "$ssl_key" \ -passin "env:SSL_KEY_PASSPHRASE" \ -out "${ssl_key}.decrypted" 2>/dev/null; then mv "${ssl_key}.decrypted" "$ssl_key" chmod 600 "$ssl_key" echo "[entrypoint] ✅ Приватный ключ расшифрован" else echo "[entrypoint] ❌ Ошибка расшифровки ключа — неверный SSL_KEY_PASSPHRASE?" rm -f "${ssl_key}.decrypted" return 1 fi } # #endregion docker.frontend.entrypoint.decrypt_key # ====================================================================== # select_nginx_config — выбор HTTP vs SSL # ====================================================================== # #region docker.frontend.entrypoint.select_nginx_config [C:3] [TYPE Function] [SEMANTICS nginx,tls,ssl,config] # @BRIEF Определяет, есть ли сертификаты для SSL, и копирует нужный nginx конфиг. # Ключ копируется как есть (если был зашифрован — decrypt_key_if_needed расшифрует после). # @PRE /opt/certs/server.crt и /opt/certs/server.key — опциональные SSL-сертификаты. # Если сертификаты были извлечены из .p12 — они уже лежат в certs_dir. # @POST Если есть оба файла — /etc/nginx/ssl/ создан, server.crt/key скопированы, # используется nginx.ssl.conf. Иначе — nginx.conf (HTTP-only). # @INVARIANT Если ключ зашифрован, он будет расшифрован decrypt_key_if_needed() # ПОСЛЕ этой функции. select_nginx_config не расшифровывает ключи самостоятельно. select_nginx_config() { local certs_dir="${CERTS_PATH:-/opt/certs}" local ssl_cert="${certs_dir}/server.crt" local ssl_key="${certs_dir}/server.key" local ssl_dir="/etc/nginx/ssl" if [ -f "$ssl_cert" ] && [ -f "$ssl_key" ]; then echo "[entrypoint] Найдены SSL-сертификаты — включаем HTTPS" mkdir -p "$ssl_dir" cp "$ssl_cert" "${ssl_dir}/server.crt" cp "$ssl_key" "${ssl_dir}/server.key" # Копируем SSL-конфиг nginx cp /docker/nginx.ssl.conf /etc/nginx/conf.d/default.conf echo "[entrypoint] ✅ SSL терминация активирована на порту 443" else echo "[entrypoint] SSL-сертификаты не найдены — используем HTTP-only режим" echo "[entrypoint] Чтобы включить HTTPS, положите в ${certs_dir}:" echo "[entrypoint] server.crt — SSL сертификат" echo "[entrypoint] server.key — приватный ключ (может быть зашифрован)" echo "[entrypoint] Или PKCS#12 с passphrase:" echo "[entrypoint] server.p12 — контейнер PKCS#12" echo "[entrypoint] SSL_KEY_PASSPHRASE — пароль от ключа/.p12" cp /docker/nginx.conf /etc/nginx/conf.d/default.conf fi } # #endregion docker.frontend.entrypoint.select_nginx_config # ── Pipeline ────────────────────────────────────────────────────────── install_ca_certificates extract_p12_if_needed ssl_passphrase="$(resolve_ssl_passphrase)" select_nginx_config decrypt_key_if_needed "$ssl_passphrase" # ── Start nginx ────────────────────────────────────────────────────── echo "[entrypoint] Starting nginx..." exec "$@"