devops-курс Все курсы

✻ Урок 2.6 · Тема 2: Сеть, HTTP, DNS, nginx и TLS

TLS и HTTPS: сертификаты и цепочка доверия

⏱ 4 ч

Зачем это нужно

Без TLS всё, что ты отправляешь по сети, идёт открытым текстом: пароли, токены (длинные секретные строки, которые работают как пароль для программ), заметки. TLS (Transport Layer Security) это набор правил, по которым две программы договариваются о шифровании и проверяют друг друга; он стоит под HTTP (урок 2.4), и страница по адресу https://... (HTTPS) это тот же HTTP, но внутри TLS. Подробно разберём ниже. Любой, кто сидит на линии (в общей Wi-Fi-сети, у провайдера, на одном из маршрутизаторов по дороге), может это прочитать или подменить. Шифрование это превращение данных в «шум», который без ключа (секретного числа для расшифровки) не прочитать. С TLS трафик зашифрован, а клиент заранее убеждается, что говорит именно с нужным сервером, а не с подставным.

На работе шифрование почти никогда не ломается само. Ломаются сертификаты. Сертификат это электронный «паспорт» сервера: файл, где написано, чей он (имя сайта), до какого числа действует и кто за него поручился. Что с ним бывает: истёк срок, в сертификате не то имя, сервер не отдал промежуточный сертификат (звено между «главным» поручителем и сервером, разберём ниже), клиент не доверяет тому, кто сертификат выдал (такого поручителя называют центром сертификации, CA, Certificate Authority). Из-за этого по ночам падают сервисы, поэтому «прочитать сертификат и найти, что с ним не так» входит в базовый набор дежурного (инженера, который в свою смену отвечает за то, чтобы сервисы работали, и первым просыпается от алерта: уведомления, что что-то сломалось). Об этом же спрашивают на собеседованиях.

Шаг проекта: «Заметки» открываются по https://notes.lab/ с самоподписанным сертификатом (его выпустили мы сами, а не известный центр сертификации: браузеры ему не поверят сами, пока мы не скажем, что ему можно верить; имя в нём notes.lab), порт 80 перенаправляет на 443 (стандартный порт HTTPS), включён HSTS (заголовок, которым сервер просит браузер: «заходи ко мне только по HTTPS, даже если ты сам набрал http»; подробно разберём ниже). Код app.py не меняется (остаётся v3 из урока 2.4), меняется только nginx и появляются файлы сертификата.

Что нужно знать

  • Урок 1.3: пользователи и права: закрытый ключ (секретный файл сервера, разберём ниже) хранится с правами 600 (читать и писать может только владелец), читает его только root (главный пользователь системы с полными правами).
  • Урок 2.1: адреса и маршруты: пакет по дороге проходит много маршрутизаторов, и каждый из них теоретически может его увидеть; адрес 127.0.0.1 (loopback) не выходит за пределы машины.
  • Урок 2.2: порты и TCP: порт, TCP-соединение, ss -ltn, отказ Connection refused.
  • Урок 2.3: DNS: запись notes.lab в /etc/hosts; имя в сертификате сверяется с именем, которое ты набрал.
  • Урок 2.4: HTTP: запрос и ответ, curl -v, curl -I, код 301 и заголовки ответа, эндпоинт /headers.
  • Урок 2.5: nginx: блоки server и location, listen, nginx -t, reload, файл ~/notes/deploy/nginx/notes.conf.

Криптографию (шифры, ключи, подписи) отдельно знать не нужно: всё необходимое объясняется в теории ниже, с нуля.

Картина целиком

Слово «ключ» тут значит не железный ключ, а длинное число, которое нужно для шифрования или расшифровки. «Подпись» это способ доказать, что документ выдал именно этот человек и его не меняли после выдачи: как печать на справке. «Рукопожатие» (handshake) это короткий обмен сообщениями в начале соединения, где стороны договариваются о шифровании. Всё это подробно разберём ниже.

Представь, что ты хочешь передать другу секретное письмо через много чужих рук. Есть две проблемы. Первая: любой почтальон может открыть конверт и прочитать. Значит, письмо надо запереть, а ключ должен быть только у тебя и у друга. Вторая: как убедиться, что на том конце настоящий друг, а не самозванец в его плаще? Для этого человек предъявляет паспорт. Паспорту веришь не потому, что он красивый, а потому, что его выдала организация, которой доверяешь ты и все вокруг.

TLS решает обе проблемы. Шифрование запирает письмо. Сертификат это паспорт сервера: в нём написано, кто он, и стоит подпись того, кто это подтвердил. Цепочка доверия это ответ на вопрос «а почему я верю организации, которая выдала паспорт?».

sequenceDiagram
    participant C as Ты (клиент)
    participant S as Сервер notes.lab
    C->>S: 1. «Привет, хочу говорить с notes.lab»
    S->>C: 2. Сертификат: имя, открытый ключ, подпись CA
    Note over C: 3. Проверяю подпись CA, срок и имя
    C->>S: 4. Договариваемся об общем секретном ключе
    C->>S: 5. Дальше весь обмен зашифрован

CA (Certificate Authority) это организация-поручитель. Клиент решает, можно ли верить подписи, так: корневой CA лежит в списке доверенных у клиента, а остальные подписаны по цепочке.

flowchart TD
    R["Корневой CA<br>в списке доверенных у клиента"] -->|"подписал"| I["Промежуточный CA"]
    I -->|"подписал"| L["Сертификат сервера notes.lab"]

Доверие идёт сверху вниз: клиент знает корень заранее, а сервер предъявляет лист и промежуточные.

За урок ты разберёшь каждый кусок: как устроено шифрование с ключами, что происходит в первую долю секунды соединения (рукопожатие), из чего состоит сертификат, как клиент проверяет цепочку, как выпустить свой сертификат и свой мини-центр сертификации и как включить всё это в nginx для «Заметок». В конце ты сломаешь сертификат тремя способами и научишься находить причину по тексту ошибки.

Теория

Зачем нужен TLS: что видит тот, кто сидит на линии

Из урока 2.1 ты знаешь, что данные едут не напрямую: пакет проходит домашний роутер, оборудование провайдера, десяток маршрутизаторов. Каждый из них технически способен скопировать пакет и прочитать. Из урока 2.4 ты знаешь, что HTTP это обычный текст. Вот реальный запрос, который nginx отправляет приложению «Заметки» (снят программой tcpdump: она показывает пакеты, которые проходят через сетевой интерфейс; ты сделаешь то же самое в задании 5):

POST /notes HTTP/1.0
Host: notes.lab

{"text":"пароль: hunter2"}

Тот, кто увидит этот пакет, прочитает и адрес, и заголовки, и пароль в теле. Без защиты возможны три атаки:

  • подслушивание: чужой читает данные;
  • подмена: чужой по дороге меняет байты (подставляет в ответ другой файл или скрипт);
  • самозванец: вместо настоящего сервера отвечает чужой, а клиент ему верит. Такую атаку называют «человек посередине» (man-in-the-middle, MITM).

TLS (Transport Layer Security, «безопасность транспортного уровня») закрывает все три: конфиденциальность (трафик зашифрован, перехватчик видит только шум), целостность (подмену байтов заметят) и аутентификация (клиент проверяет, что сервер тот, за кого себя выдаёт).

Аналогия: обычный HTTP это открытка, которую читает каждый почтальон. TLS это запечатанный конверт, на котором стоит печать нотариуса, подтверждающая, кто отправитель. Оговорка: снаружи конверта всё равно виден адрес. В TLS тоже видно, к какому IP и порту ты подключился, и (как ты увидишь ниже) даже имя сайта.

HTTPS это обычный HTTP, упакованный внутрь TLS. Запросы, заголовки и коды те же самые, что в уроке 2.4, поменялся только слой под ними. Порт по умолчанию 443 (у HTTP 80).

flowchart LR
    subgraph A["Без TLS"]
        A1["HTTP<br>текст запроса"] --> A2["TCP"] --> A3["IP"] --> A4["Сеть"]
    end
    subgraph B["С TLS"]
        B1["HTTP"] --> B2["TLS<br>шифрование"] --> B3["TCP"] --> B4["IP"] --> B5["Сеть"]
    end

Добавляется один слой между HTTP и TCP: запросы и коды остаются прежними, но по сети идёт шифрованный поток.

Слово SSL (Secure Sockets Layer) ты встретишь в названиях: директива ssl_certificate, пакет libssl, фраза «SSL-сертификат». Так называлась предыдущая технология. Версии SSL 2.0 и 3.0 давно взломаны и везде отключены, сегодня работают TLS 1.2 и TLS 1.3, но привычное слово осталось. Когда говорят «SSL-сертификат», имеют в виду обычный сертификат для TLS.

Порядок при открытии https://notes.lab/: сначала DNS (урок 2.3) превращает имя в адрес, потом устанавливается TCP-соединение на порт 443 (урок 2.2), потом идёт рукопожатие TLS (handshake, о нём ниже), и только потом отправляется HTTP-запрос (урок 2.4). Поэтому ошибка сертификата видна до того, как запрос вообще ушёл.

Осторожно: замочек в браузере и https:// не значат «сайт честный». Они значат две вещи: канал между тобой и сервером защищён, и имя в адресной строке подтверждено сертификатом. Мошенник может купить сертификат на своё поддельное имя, и замочек у него тоже будет. TLS защищает дорогу, а не намерения владельца.

Прикинь сам: какие три атаки возможны при обычном HTTP?

Подслушивание, подмена данных по дороге и самозванец вместо настоящего сервера (MITM).

Главное: TLS даёт конфиденциальность, целостность и проверку сервера. HTTPS это HTTP внутри TLS, порт 443.

Проверь понимание: на каком шаге пути запроса ты увидишь ошибку сертификата: до или после HTTP-запроса? Почему curl тогда не покажет код ответа?

Ответ

До. Рукопожатие TLS идёт после TCP и до HTTP. Если клиент не принял сертификат, он закрывает соединение, HTTP-запрос не отправляется, и кода ответа просто нет: curl завершается с ошибкой.

Теперь ясно, от чего защищает TLS. Дальше разберём, на чём он держится: как две стороны договариваются о секретном ключе, когда линию слушают чужие.

Ключи: как договориться о секрете, когда вокруг чужие

Чтобы зашифровать данные, нужен ключ. Ключ это длинное секретное число, которое управляет шифрованием: с одним ключом текст превращается в одну «кашу», с другим в другую. Тут и возникает главная трудность: чтобы клиент и сервер зашифровали разговор, им нужен общий секретный ключ, а передать его по открытой линии нельзя, его увидят. Это похоже на ситуацию «запереть сейф, а ключ отдать почтальону». Задачу решают две разновидности шифрования, и TLS использует обе.

Симметричное шифрование. Один и тот же ключ и запирает, и отпирает. Самый простой пример из истории, шифр Цезаря: каждая буква заменяется на букву через три позиции в алфавите. Ключ здесь число 3.

открытый текст:   n  o  t  e  s
сдвиг на 3:       n->q  o->r  t->w  e->h  s->v
шифротекст:       q  r  w  h  v          (расшифровка: сдвинуть назад на 3)

Настоящие шифры (в TLS это AES) устроены несравнимо сложнее, но принцип тот же: один ключ на обе стороны. Плюс: очень быстро, можно шифровать гигабайты. Минус: ключ нужно как-то передать собеседнику безопасно, а это и есть исходная проблема.

Асимметричное шифрование. Тут не один ключ, а пара, которую создают вместе: открытый ключ (public key) и закрытый ключ (private key). Они математически связаны: то, что запёрто одним, отпирается только другим. Открытый ключ можно раздавать всем, закрытый хозяин не показывает никому и никогда.

Аналогия: почтовый ящик с щелью. Открытый ключ это щель: любой прохожий может бросить в неё письмо. Закрытый ключ это ключ от дверцы: достать письма может только хозяин. Оговорка: в настоящем ящике щель и дверца разные части, а в математике пара ключей симметрична: можно и наоборот, запереть закрытым, а отпереть открытым. Это свойство нужно для подписи.

Минус асимметричного шифрования: оно в сотни и тысячи раз медленнее симметричного. Поэтому в TLS его используют только для двух коротких задач: договориться об общем секрете и подтвердить личность (подпись). А сами данные потом шифруют быстрым симметричным ключом. Так и решается исходная трудность: асимметричная часть помогает безопасно получить общий симметричный ключ.

Подпись. Как доказать, что документ написал именно ты и что его не меняли? Сначала документ превращают в хеш (hash), короткую строку фиксированной длины, «отпечаток» содержимого. Свойства хеша: из одних и тех же данных всегда получается одна и та же строка; малейшая правка данных даёт совсем другую строку; по хешу данные восстановить нельзя. Посмотри на настоящем примере (sha256sum считает хеш SHA-256, echo -n печатает текст без перевода строки):

echo -n "notes"  | sha256sum   ->  ab5aa97074c454a0632057e704220d9a6678fbf773a0a5806fc09b8173b07309
echo -n "Notes"  | sha256sum   ->  8a7525b1492fb84833f5c4a69b30f4bfbb134f9b666b61a2c1872d63d234c085
echo -n "notes " | sha256sum   ->  391867a6cbe18f6a586ac6882a784cc8719e5906500d77f50e47b7f2bb624075

Разница в одну заглавную букву или в один пробел, и хеш не похож на предыдущий ни одним куском. Длина всегда 64 символа (это 256 бит, записанные в шестнадцатеричной системе: по 4 бита на символ).

Подписать документ значит вычислить его хеш и зашифровать этот хеш закрытым ключом. Проверить подпись может любой: расшифровывает подпись открытым ключом автора, сам считает хеш документа, и если два хеша совпали, значит, документ подписан хозяином закрытого ключа и после подписи не менялся. Подделать подпись без закрытого ключа нельзя. В задании 3 ты подпишешь файл руками и увидишь, как проверка ломается от одной изменённой буквы.

Осторожно: открытый ключ не секрет, его отдают всем, в этом весь смысл. Секрет только закрытый: его нельзя копировать в чат, класть в git или давать права шире 600 (урок 1.3). Если закрытый ключ сервера утёк, любой может выдавать себя за этот сервер, и сертификат придётся выпускать заново с новой парой ключей.

Прикинь сам: если в имени сайта поменять одну заглавную букву, изменится ли хеш?

Да, и целиком: хеш перестанет быть похожим на прежний. Длина при этом останется той же (64 символа для SHA-256).

Главное: открытый ключ отдают всем, секретен только закрытый. Подпись это хеш документа, зашифрованный закрытым ключом, а проверяют её открытым.

Проверь понимание: почему нельзя просто шифровать весь трафик асимметричным способом, без симметричного ключа? И что из пары «открытый и закрытый ключ» можно безопасно отдавать всем?

Ответ

Асимметричное шифрование очень медленное, гигабайты им не зашифровать за разумное время. Поэтому им только договариваются об общем ключе и подписывают, а данные шифруют быстрым симметричным способом. Безопасно отдавать всем открытый ключ. Закрытый ключ секретный.

Ключи у каждой стороны есть, но договориться с их помощью нужно за доли секунды, пока соединение только начинается. Как это происходит шаг за шагом, показывает следующий раздел.

Рукопожатие: что происходит в первые миллисекунды

Пока клиент и сервер не договорились о ключе, они не могут шифровать. Обмен сообщениями, в котором они договариваются, называется рукопожатием (handshake). Оно идёт после установки TCP-соединения и до первого HTTP-запроса. Вот его шаги для TLS 1.3 (актуальная версия):

  1. ClientHello. Клиент сообщает: какие версии TLS и шифры он умеет, свою половину данных для обмена ключами и имя сервера (SNI, Server Name Indication).
  2. ServerHello. Сервер выбирает версию и шифр из предложенного и присылает свою половину данных для обмена ключами. С этого момента обе стороны могут посчитать общий секретный ключ, и всё дальнейшее шифруется.
  3. Certificate. Сервер (уже зашифрованно) отдаёт свой сертификат и подпись своего закрытого ключа под всем, что было сказано в рукопожатии (в выводе curl она называется CERT verify). Так он доказывает, что владеет закрытым ключом, соответствующим сертификату.
  4. Finished. Обе стороны шлют друг другу итоговое сообщение, которое подтверждает, что они пришли к одному и тому же ключу и никто не вмешался. Дальше идут зашифрованные HTTP-запросы и ответы.

Клиент между шагами 3 и 4 проверяет сертификат (следующие разделы). Если проверка не прошла, клиент закрывает соединение, и до HTTP дело не доходит.

sequenceDiagram
    participant C as Клиент
    participant S as Сервер
    C->>S: ClientHello: версии, шифры, SNI = notes.lab
    S->>C: ServerHello: версия, шифр, половина ключа
    Note over C,S: с этого места всё шифруется общим ключом
    S->>C: Certificate: сертификат сервера
    S->>C: CERT verify: подпись закрытым ключом сервера
    S->>C: Finished
    C->>S: Finished
    C->>S: GET /healthz (зашифрованный HTTP)

Рукопожатие занимает один обмен в обе стороны, и только после него идёт сам HTTP.

Как договариваются об общем ключе, не передавая его. Для этого придумали обмен ключами Диффи-Хеллмана (Diffie-Hellman). Аналогия с красками: обе стороны заранее знают общий цвет (жёлтый) и публично называют его. Каждый тайно выбирает свой цвет (клиент красный, сервер синий), смешивает его с жёлтым и отправляет смесь. Перехватчик видит две смеси, но не может «вынуть» из них секретные краски. Каждая сторона добавляет свой секретный цвет к смеси собеседника, и у обоих получается один и тот же итоговый цвет: жёлтый + красный + синий. Это и есть общий ключ, по проводу он не шёл. Оговорка: в жизни краски смешиваются в любом порядке и разделить смесь нельзя, а в математике «нельзя» означает «не хватит времени всех компьютеров мира».

Та же идея на маленьких числах. Общие публичные числа: основание 5 и модуль 23. Секрет клиента 6, секрет сервера 15.

клиент отправляет:  5^6  mod 23 = 15625 mod 23 = 8      (15625 = 23*679 + 8)
сервер отправляет:  5^15 mod 23 = 19                    (5^15 = 30517578125 = 23*1326851222 + 19)
клиент считает:     19^6 mod 23 = 2     (секрет клиента 6 применён к присланному 19)
сервер считает:     8^15 mod 23 = 2     (секрет сервера 15 применён к присланному 8)
общий ключ: 2. Перехватчик знает 5, 23, 8 и 19, но не 6 и не 15.

Операция «mod 23» значит «остаток от деления на 23». Настоящие ключи в тысячи раз длиннее, а вместо степеней по модулю используют кривые (в выводе ты увидишь название X25519), но идея одна.

Разбор реального рукопожатия. Вот что печатает curl -v при соединении с nginx из этого урока (вывод настоящий, из прогона на Ubuntu 24.04 с curl 8.5.0). Значки (IN) и (OUT) показывают направление: входящее от сервера или исходящее от клиента.

* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS

Строки соответствуют шагам выше (Encrypted Extensions это уточнения параметров, которые сервер шлёт уже зашифрованно). Последняя строка читается слева направо: TLSv1.3 версия протокола; TLS_AES_256_GCM_SHA384 набор шифров (AES с 256-битным ключом для данных, режим GCM, хеш SHA-384 для проверки целостности); X25519 кривая для обмена ключами по Диффи-Хеллману; RSASSA-PSS алгоритм подписи, которой сервер доказал владение ключом.

Отдельно про имя в ClientHello (SNI). На одном IP-адресе и порту 443 nginx может обслуживать десять сайтов, у каждого свой сертификат. Но выбрать сертификат надо до того, как придёт HTTP-заголовок Host (ведь HTTP-запрос ещё зашифрован). Поэтому клиент называет нужное имя прямо в ClientHello, открытым текстом. Это видно и перехватчику: даже при HTTPS чужой узнает, на какой сайт ты зашёл, но не узнает, что ты там делал. Клиент, который не передал SNI (например, openssl s_client без -servername или запрос по голому IP), получит сертификат «по умолчанию»: nginx отдаёт сертификат первого server или того, где стоит default_server. В задании 4 ты увидишь это своими глазами.

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

Прикинь сам: на каком этапе рукопожатия клиент и сервер получают общий секретный ключ, не передавая его?

Во время обмена ключами (по Диффи-Хеллману): каждая сторона вычисляет ключ у себя, а перехватчик видит лишь публичные числа.

Главное: рукопожатие это ClientHello, ServerHello, сертификат, проверка подписи и Finished. После него HTTP идёт зашифрованным.

Проверь понимание: в ClientHello имя сайта идёт открытым текстом. Что из этого видит перехватчик, а что нет?

Ответ

Видит: IP сервера, порт 443 и имя сайта (SNI), то есть «этот человек зашёл на notes.lab». Не видит: путь (/notes), заголовки, тело запроса, ответ. Всё это идёт после рукопожатия внутри шифрованного соединения.

В рукопожатии сервер предъявляет сертификат. Что в нём написано и почему ему можно верить, разберём дальше.

Сертификат: паспорт сервера

Клиент получил от сервера открытый ключ. Но откуда он знает, что ключ принадлежит именно notes.lab, а не самозванцу, который подсунул свой? Нужен документ, где кто-то, кому клиент доверяет, подтвердил: «этот открытый ключ принадлежит этим именам». Такой документ и есть сертификат (в разговоре: cert, «серт»). Формат называется X.509.

Аналогия: паспорт. В нём написано, чей он, до какого числа действителен, кто выдал, и стоит печать. Оговорка: паспорт хранит фото, по которому тебя узнают, а сертификат хранит открытый ключ, а «узнаёт» сервер себя подписью закрытого ключа (шаг рукопожатия CERT verify).

Вот настоящий сертификат notes.lab, который ты выпустишь в задании 2 (показаны важные поля, вывод команды openssl x509 -noout -text, значения у тебя будут другие):

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            34:4c:74:a0:52:d0:a3:5c:76:4d:5a:6c:a4:c5:00:94:98:4c:50:c3
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = notes.lab
        Validity
            Not Before: Sep 30 11:52:40 2026 GMT
            Not After : Sep 30 11:52:40 2027 GMT
        Subject: CN = notes.lab
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Subject Alternative Name:
                DNS:notes.lab
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        05:1b:68:e7:30:03:cb:31:...

Разбор по строкам:

  • Serial Number: номер, уникальный у каждого сертификата от одного издателя. По нему сертификат можно отозвать или найти в логах.
  • Issuer (издатель): кто выдал и подписал сертификат.
  • Subject (субъект): кому выдан. В нём есть поле CN (Common Name, «общее имя»): исторически туда писали имя сайта. Сегодня CN считается устаревшим, роль имён играет SAN.
  • Validity (срок): Not Before (не раньше) и Not After (не позже). Вне этого окна сертификат недействителен. Здесь ровно 365 дней.
  • Subject Public Key Info: открытый ключ владельца (RSA, 2048 бит). Закрытого ключа в сертификате нет и быть не может.
  • Basic Constraints: CA:TRUE/FALSE: может ли владелец сам подписывать другие сертификаты (CA, о нём ниже). У самоподписанного сертификата, выпущенного командой из задания 2, здесь CA:TRUE: он подписал сам себя, поэтому формально сам себе центр сертификации.
  • Subject Alternative Name (SAN): главное поле. Это список имён, на которые действует сертификат: DNS:notes.lab, DNS:*.example.com (звёздочка означает «любой поддомен»), IP:192.168.64.5. Именно по SAN клиент сверяет то, что ты набрал в адресе.
  • Signature Value: подпись издателя под всем сертификатом. Именно её проверяет клиент.

Сам файл выглядит так: строки -----BEGIN CERTIFICATE----- и -----END CERTIFICATE-----, а между ними текст из букв и цифр. Это формат PEM: двоичные данные сертификата, записанные в base64 (способ записать любые байты обычными буквами, цифрами, +, /, чтобы файл можно было открыть в текстовом редакторе и вставить в письмо). Читать base64 глазами не нужно: для этого есть openssl x509 -text.

SAN против CN: что на самом деле проверяется. Браузеры (Chrome, Firefox и другие) давно сверяют адрес только с SAN и полностью игнорируют CN. Если в SAN нет нужного имени, это ошибка, даже если в CN оно написано правильно. Утилиты чуть снисходительнее: если в сертификате SAN нет вообще, curl и openssl verify ещё заглядывают в CN. Мы проверили это на стенде: сертификат с CN=notes.lab и без SAN curl из Ubuntu 24.04 принял, а сертификат с SAN DNS:www.notes.lab отклонил, хотя CN верный. Поэтому правило одно: всегда пиши имя в SAN, иначе сертификат заработает в curl и сломается в браузере.

Осторожно: сертификат не содержит закрытого ключа и не секретен: его сервер отдаёт каждому клиенту при каждом соединении. Секретен только файл ключа (.key).

Прикинь сам: в сертификате есть имя notes.lab. Подойдёт ли он, если открыть сайт по IP-адресу?

Нет, если IP не указан в SAN: клиент сверяет набранный адрес со списком SAN.

Главное: сертификат это паспорт сервера: имена (SAN), открытый ключ, срок и подпись издателя.

Проверь понимание: в SAN стоит только DNS:notes.lab. Подойдёт ли такой сертификат, если открыть сайт по https://192.168.64.5/? А по https://www.notes.lab/?

Ответ

Нет в обоих случаях. Сверка идёт по списку SAN строго: IP-адреса 192.168.64.5 и имени www.notes.lab там нет, клиент откажется. Чтобы сработали оба варианта, их нужно добавить в SAN: DNS:notes.lab,DNS:www.notes.lab,IP:192.168.64.5.

Сертификат подтверждает ключ, но не навсегда: у него есть срок. Почему он ограничен и что с этим делать, следующий раздел.

Срок действия: почему сертификаты «протухают»

Если ключ украден, а сертификат вечный, самозванец будет пользоваться им вечно. Срок ограничивает ущерб: по его окончании нужно получить новый сертификат, а значит, снова подтвердить, что ты владелец. Поэтому сертификаты публичных сайтов живут всё меньше. У example.com в прогоне для этого урока срок 90 дней (с 26 сентября по 25 декабря 2026), и по решению CA/Browser Forum (организации, в которую входят браузеры и центры сертификации) предельный срок и дальше сокращается, до 47 дней к 2029 году. Точные даты проверяй на сайте cabforum.org. Отсюда вывод: руками сертификаты не продлевают, это автоматизируют. Как, разберём в уроке 9.4 (cert-manager).

Как проверяется. Клиент сравнивает свои часы с окном Not Before и Not After. Поэтому сертификат «просрочен» и когда его срок правда закончился, и когда часы клиента убежали вперёд, и (реже) когда сервер выпустил сертификат «на будущее», а часы клиента ещё не дошли до Not Before. Значит, при странной ошибке срока сначала посмотри date -u на обеих сторонах.

Возьмём конкретные даты. Сертификат notes.lab из задания 2 действителен с 30 сентября 2026 11:52:40 по 30 сентября 2027 11:52:40 (все времена в GMT, то есть UTC). Команда openssl x509 -checkend N отвечает на вопрос «останется ли сертификат действительным ещё N секунд?». Ответ передаётся кодом выхода: 0 значит «да» (текст Certificate will not expire), 1 значит «нет» (Certificate will expire). Для 30 дней N = 30 × 86400 = 2 592 000 секунд: сертификат, которому осталось 365 дней, ответит «не истечёт». Для 400 дней N = 400 × 86400 = 34 560 000: ответит «истечёт».

Осторожно: «сертификат просрочен» и «сертификат не подходит по имени» выглядят для пользователя одинаково (страница с предупреждением), но лечатся по-разному. Различай их по тексту ошибки, таблица в конце теории.

Прикинь сам: сертификат истёк вчера, а ключ и имя верные. Откроется ли сайт?

Нет: клиент сверяет срок по своим часам и рвёт соединение с ошибкой certificate has expired.

Главное: сертификаты «протухают» намеренно, чтобы украденный ключ не жил вечно. Срок проверяют заранее через -checkend.

Проверь понимание: сертификат выпущен на 365 дней. Через какое время в секундах команда -checkend начнёт отвечать «Certificate will expire» на N = 2 592 000?

Ответ

Когда до конца срока останется меньше 30 дней (2 592 000 секунд), то есть на 336-й день жизни сертификата. Именно поэтому в мониторинге ставят алерт за 14-30 дней до конца: времени хватает, чтобы спокойно продлить.

Срок проверить просто. Сложнее другой вопрос: почему клиент вообще верит подписи издателя. На него отвечает цепочка доверия.

Цепочка доверия: почему клиент верит подписи

Мы сказали: сертификат подписан издателем, и клиент проверяет подпись. Но откуда клиент знает открытый ключ издателя и почему ему верит? Знать всех издателей мира невозможно, а каждый новый сайт не может присылать клиенту личную встречу. Выход: клиент заранее доверяет небольшому набору организаций, а те подтверждают остальных, как по цепочке поручительств.

Представь, что ты приходишь в банк, показываешь паспорт. Паспорту веришь не потому, что он красивый, а потому, что его выдало МВД, а МВД признаёт государство, которому доверяют все. Если у тебя вместо паспорта справка из ЖЭКа, которую ЖЭК выдал сам себе, банк ей не поверит. Оговорка: в отличие от государства, в интернете «государств» много, и каждый браузер и каждая ОС держит свой список доверенных.

В цепочке три уровня:

  • Корневой центр сертификации (root CA, CA это Certificate Authority): организация, которой доверяют по умолчанию. Её сертификат самоподписанный (издатель равен субъекту) и заранее лежит в списке доверенных у клиента. Ключ корня хранят офлайн, в сейфе, им подписывают редко.
  • Промежуточный центр (intermediate CA): подписан корнем, а уже он ежедневно подписывает сертификаты сайтов. Так корневой ключ почти не используется и почти не рискует утечкой, а если утечёт ключ промежуточного, отзывают только его.
  • Конечный сертификат (leaf, «лист»): сертификат сервера, например notes.lab. Подписан промежуточным.
flowchart TD
    R["Корневой CA<br>Issuer = Subject (сам себя подписал)<br>лежит в хранилище клиента"] -->|"подписал"| I["Промежуточный CA<br>Issuer = корневой"]
    I -->|"подписал"| L["Лист notes.lab<br>Issuer = промежуточный<br>сервер отдаёт его клиенту"]

Клиент идёт по цепочке снизу вверх и должен дойти до корня из своего хранилища.

Что делает клиент, получив сертификат сервера. Он идёт по цепочке снизу вверх и проверяет:

  1. Подпись. Подпись листа проверяется открытым ключом промежуточного, подпись промежуточного открытым ключом корня.
  2. Дошли ли до корня из хранилища. Последний сертификат должен совпасть с одним из доверенных. Если цепочка оборвалась, не дойдя до знакомого корня, ошибка (unable to get local issuer certificate).
  3. Срок каждого сертификата в цепочке по часам клиента.
  4. Имя. Адрес, который ты набрал, должен быть в SAN листа.
  5. Не отозван ли сертификат (специальные списки отзыва, отдельная тема, в курсе мы её не разбираем).

Хоть одна проверка не прошла, и клиент рвёт соединение.

Где хранится список доверенных. Он называется хранилищем доверенных сертификатов (trust store). В Ubuntu это каталог /etc/ssl/certs/ и общий файл /etc/ssl/certs/ca-certificates.crt, который собирает пакет ca-certificates. На стенде для урока в нём 121 корневой сертификат, у тебя число может отличаться. У браузеров (Firefox, Chrome) есть собственные списки, и они не обязаны совпадать с системным, поэтому «в браузере открывается, а curl ругается» встречается.

Разбор реальной цепочки. Вот что отдал example.com (вывод openssl s_client, s: это subject, i: это issuer; вывод настоящий, у тебя будет другой, потому что сайт обновляет сертификаты):

 0 s:CN = example.com
   i:C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
 1 s:C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
   i:C = US, O = SSL Corporation, CN = SSL.com TLS Transit ECC CA R2
 2 s:C = US, O = SSL Corporation, CN = SSL.com TLS Transit ECC CA R2
   i:C = US, O = SSL Corporation, CN = SSL.com TLS ECC Root CA 2022
 3 s:C = US, O = SSL Corporation, CN = SSL.com TLS ECC Root CA 2022
   i:C = GB, ST = Greater Manchester, L = Salford, O = Comodo CA Limited, CN = AAA Certificate Services

Видно звенья: у каждого i: (кто выдал) равен s: (кому выдан) следующего. Лист example.com подписан Cloudflare TLS Issuing ECC CA 3, тот подписан SSL.com TLS Transit ECC CA R2, и так вверх. Последнее звено интересно: корень SSL.com TLS ECC Root CA 2022 здесь подписан ещё одним, более старым корнем AAA Certificate Services. Это называется перекрёстная подпись (cross-signing): новому корню, о котором старые устройства ещё не знают, «поручился» старый, известный им. Клиент доходит до любого знакомого корня по этому пути и останавливается.

Что должен отдавать сервер. Сервер обязан прислать лист вместе со всеми промежуточными. Корень присылать не нужно: он и так есть у клиента. Файл, где лист и промежуточные склеены один за другим, называют fullchain.pem (полная цепочка). Частая авария: настроили только лист. Браузер на ноутбуке может достать недостающий промежуточный из кэша или по адресу, записанному в сертификате (расширение AIA, Authority Information Access), и всё работает, а curl, скрипты и мобильные приложения падают с unable to get local issuer certificate. В задании 4 ты воспроизведёшь эту аварию.

Осторожно: корень не нужно класть на сервер, клиент его знает сам. И «сертификат подписан известным центром» не значит «сайту можно верить», он значит только «имя подтверждено».

Прикинь сам: сервер прислал только лист, без промежуточного сертификата. Что скажут клиенты?

Часть клиентов не сможет дойти до корня и выдаст unable to get local issuer certificate. Сервер обязан присылать лист вместе с промежуточными.

Главное: клиент идёт по цепочке снизу вверх и проверяет подпись, корень из хранилища, срок и имя. Корень на сервер класть не нужно.

Проверь понимание: сертификат есть, срок не вышел, имя верное, но curl пишет unable to get local issuer certificate. Что это значит и что проверишь первым?

Ответ

Клиент не смог построить цепочку до доверенного корня. Либо сервер не отдал промежуточный сертификат (проверяю openssl s_client -showcerts: сколько сертификатов в цепочке), либо издателя (например, внутреннего CA компании) нет в хранилище клиента.

Цепочка объясняет, почему публичный центр сертификации не подойдёт для внутреннего имени вроде notes.lab. Что делать на стенде, расскажет следующий раздел.

Самоподписанный сертификат и свой центр сертификации

Публичный центр сертификации не выдаст сертификат на notes.lab: этого имени нет в настоящем интернете (урок 2.3), проверить владение нечем. Для учебного стенда и внутренних сервисов компании нужен свой способ.

Самоподписанный сертификат подписан собственным закрытым ключом: издатель равен субъекту. Ни в чьём хранилище его нет, поэтому клиент по умолчанию откажет с ошибкой self-signed certificate. Есть два способа сделать так, чтобы клиент принял его:

  • указать клиенту сертификат как доверенный для одного запроса: curl --cacert notes.crt. Так мы будем делать в проекте;
  • положить его в хранилище системы: скопировать файл в /usr/local/share/ca-certificates/ (имя обязательно с расширением .crt) и выполнить sudo update-ca-certificates. После этого все программы системы, которые читают общее хранилище, ему поверят. Ты сделаешь это в задании 4.

Для нескольких сервисов самоподписанные сертификаты неудобны: у каждого свой, клиентам надо раздавать каждый. Поэтому в компаниях заводят свой мини-CA: один корневой ключ и сертификат, которые подписывают сертификаты всех сервисов. Клиентам раздают один корень, и он открывает все сервисы сразу. Как получить листовой сертификат от CA, пошагово: клиент создаёт пару ключей, делает CSR (Certificate Signing Request, запрос на подпись: файл с открытым ключом и именем, но без закрытого ключа), отдаёт его CA, а тот возвращает подписанный сертификат. Закрытый ключ при этом никуда не уходит. Всё это ты пройдёшь руками в задании 3. Автоматизированный вариант для Kubernetes разберём в уроке 9.4.

Ключ curl -k (--insecure) полностью отключает проверку сертификата. Это не «обойти ошибку», а отказ от аутентификации: шифрование остаётся, но говорить можно с кем угодно, в том числе с самозванцем. В скриптах на проде -k красный флаг, а в этом курсе мы им не лечим ничего.

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

Издатель совпадает с владельцем, и ни один корень из хранилища клиента его не подтверждает. Нужно добавить его в доверенные вручную.

Главное: свой центр сертификации подходит для внутренних имён: клиенту достаточно доверять одному корню.

Проверь понимание: почему клиенту достаточно доверять одному корню, а не каждому сервисному сертификату? Что придётся делать при выпуске нового сервиса?

Ответ

Клиент проверяет подпись и идёт до корня, который знает. Любой сертификат, подписанный этим корнем (напрямую или через промежуточный), проходит проверку. Для нового сервиса нужно только выпустить ему сертификат у CA: клиентам ничего раздавать не надо.

Сертификат для стенда у нас теперь есть. Осталось подключить его к nginx из прошлого урока.

HTTPS в nginx: терминация TLS, редирект, HSTS

Приложение «Заметки» на Python не умеет шифровать. Учить его этому в каждом сервисе незачем: nginx, который уже стоит перед приложением (урок 2.5), берёт шифрование на себя. Такое разделение труда называют терминацией TLS (TLS termination): TLS начинается на клиенте и заканчивается («терминируется») на nginx, а дальше запрос идёт к приложению обычным HTTP.

интернет                            одна машина
                       nginx                         приложение
клиент ==TLS 443==>  [расшифровка] --HTTP 127.0.0.1:8080--> «Заметки»
       (зашифровано)                (открытый текст, но только
                                     внутри машины: наружу не выходит)

Открытый текст на участке nginx → приложение не дыра: он идёт по loopback (урок 2.1) и физически не выходит за пределы машины. Вот почему приложение по-прежнему слушает 127.0.0.1:8080: иначе клиенты могли бы обойти и TLS, и nginx. В задании 5 ты убедишься в этом глазами: на участке 443 пароль не виден, на участке 8080 виден.

Как это записывается. Два блока server:

  • на порту 80 стоит только return 301 https://$host$request_uri;: любой HTTP-запрос получает ответ 301 (урок 2.4) с адресом Location, где схема заменена на https. $host это имя из запроса, $request_uri путь и параметры, поэтому http://notes.lab/healthz превращается в https://notes.lab/healthz;
  • на порту 443 стоит listen 443 ssl; (ssl включает TLS на этом порту), ssl_certificate (файл сертификата, для цепочки это fullchain), ssl_certificate_key (файл закрытого ключа) и ssl_protocols TLSv1.2 TLSv1.3; (разрешённые версии: старее не пускаем). Дальше всё как в уроке 2.5: location с proxy_pass.

Ключ читает главный процесс nginx (master), а он запущен от root: ты это увидишь в ps. Поэтому права 600 root:root на файл ключа не мешают работе, а рабочие процессы (worker, от пользователя www-data) ключ читать не могут, и это хорошо.

HSTS. Пользователи набирают notes.lab без схемы, и браузер идёт по HTTP. Редирект 301 спасает, но между первым запросом и редиректом есть окно: в это время атакующий может перехватить открытый запрос и не пустить пользователя на HTTPS. Заголовок ответа Strict-Transport-Security: max-age=31536000 (HSTS, HTTP Strict Transport Security) закрывает окно: браузер запоминает на max-age секунд (31 536 000 секунд = 365 дней), что этот сайт ходит только по HTTPS, и сам переписывает адрес на https://, вообще не делая открытого запроса. Браузер принимает заголовок только по HTTPS (по HTTP его мог бы подделать кто угодно).

Ловушка HSTS: пока действует запись, браузер не даст пользователю нажать «всё равно продолжить» при ошибке сертификата. Значит, просроченный сертификат при большом max-age делает сайт совсем недоступным для этих пользователей. Поэтому начинают с малого (max-age=300), убеждаются, что автопродление работает, и только потом ставят год.

Слово always в add_header Strict-Transport-Security "..." always; означает, что nginx добавит заголовок к любому ответу. Без него заголовок будет только у ответов 2xx и 3xx, а у 4xx и 5xx исчезнет.

Осторожно: не думай, что «раз есть HSTS, редирект не нужен», и наоборот. Нужны оба: редирект обслуживает первый визит и клиентов без HSTS (curl, скрипты, старые браузеры), HSTS защищает все последующие визиты.

Прикинь сам: где nginx расшифровывает трафик, а приложение получает обычный HTTP?

На самом nginx: это терминация TLS. До него идёт шифрованный поток, дальше приложению обычный HTTP.

Главное: nginx завершает TLS, редирект с 80 на 443 переводит на https, а HSTS запоминается в браузере.

Проверь понимание: зачем редирект, если есть HSTS, и зачем HSTS, если есть редирект?

Ответ

Редирект нужен для первого визита и для клиентов, которые не знают про HSTS. HSTS закрывает окно атаки на последующих визитах: браузер сам меняет адрес на https и не делает открытый запрос.

В конфиге nginx ты указываешь файлы с сертификатом и ключом, а перепутать их легко. Что за файлы бывают и чем они отличаются, разберём дальше.

Файлы: что такое .pem, .crt, .key, .csr

При работе с TLS у тебя на диске появляется куча файлов с похожими расширениями, и любое перепутывание даёт непонятные ошибки. Расширение это просто договорённость об имени, а вот содержимое определяется форматом.

Файлы различаются по двум признакам: формату записи и назначению.

  • Формат PEM (текст): между строками -----BEGIN ...----- и -----END ...----- лежат данные в base64. Файл можно открыть редактором, вставить в письмо, склеить командой cat. Формат DER (двоичный): те же данные без обёртки, читается только программами.
  • По строке BEGIN ты понимаешь, что внутри: BEGIN CERTIFICATE сертификат, BEGIN PRIVATE KEY закрытый ключ, BEGIN CERTIFICATE REQUEST запрос на подпись (CSR).
  • Расширения: .crt и .cer обычно сертификат (чаще PEM), .key закрытый ключ, .csr запрос, .pem «любой PEM» (иногда сертификат, иногда цепочка, иногда ключ: смотри строку BEGIN). .pfx и .p12 это архив, где сертификат и закрытый ключ лежат вместе, защищённые паролем; его любят Windows и Java.

Пример. Посмотри первую строку своих файлов из задания 2:

head -n 1 /etc/notes/tls/notes.crt
sudo head -n 1 /etc/notes/tls/notes.key
-----BEGIN CERTIFICATE-----
-----BEGIN PRIVATE KEY-----

Расширение может врать (файл cert.pem бывает ключом), а строка BEGIN нет. Файл fullchain.pem из задания 3 содержит две строки BEGIN CERTIFICATE: это две «страницы» в одном файле.

Осторожно: для nginx не хватит одного файла. Нужны два: сертификат (или fullchain) в ssl_certificate и ключ в ssl_certificate_key. И не клади ключ рядом с сертификатом «для удобства»: ключ живёт отдельно, с правами 600.

Прикинь сам: расширение .pem говорит, что внутри сертификат?

Нет, это лишь формат записи. Сертификат, ключ и запрос могут быть в .pem, смотри первую строку файла.

Главное: .pem это формат, а содержимое определяет строка BEGIN .... Закрытый ключ хранят с правами 600.

Проверь понимание: тебе прислали файл server.pem. Как без специальных программ понять, сертификат это, ключ или цепочка?

Ответ

Открыть первые строки (head) и прочитать BEGIN .... Если несколько строк BEGIN CERTIFICATE, это цепочка (grep -c 'BEGIN CERTIFICATE' server.pem покажет сколько). Если BEGIN PRIVATE KEY, то это закрытый ключ, и такой файл нельзя пересылать.

Файлы разложены по местам. Остаётся решить, какие версии TLS разрешить, и об этом следующий раздел.

Версии TLS и прямая секретность

Технология менялась, и старые версии содержат уязвимости. Поэтому в конфиге стоит ssl_protocols TLSv1.2 TLSv1.3;: разрешены две актуальные версии, а всё, что старше (SSL 3.0, TLS 1.0 и 1.1), отключено. Понимание разницы нужно, чтобы ответить на вопрос «а почему нельзя оставить всё включённым».

С версиями происходит то же, что с замками на двери. Старые замки взломали, их снимают с продажи, но пока в доме остался один такой, вор придёт именно к нему. Оговорка: в отличие от замка, версия протокола выбирается при каждом соединении, и сервер с разрешённой старой версией вынужден иногда отвечать старым клиентам, а атакующий может попытаться навязать слабейший вариант.

Версии отличаются так, а прямая секретность означает следующее:

  • TLS 1.2 (2008): рукопожатие занимает два обмена туда-обратно после установки TCP. Сертификат сервера идёт открытым текстом.
  • TLS 1.3 (2018): один обмен, а сертификат уже зашифрован. Убраны устаревшие шифры. Ты видел это в curl -v: строка Encrypted Extensions и есть шифрование части рукопожатия.
  • Прямая секретность (forward secrecy): для каждого соединения используется новая одноразовая пара ключей для обмена Диффи-Хеллмана, а закрытый ключ сертификата нужен только для подписи. Если ключ сервера когда-нибудь украдут, старые записанные разговоры расшифровать не получится: общий секрет каждого из них уже стёрт. В TLS 1.3 прямая секретность обязательна.

Пример с арифметикой. Пусть от тебя до сервера 50 мс в одну сторону, то есть обмен туда-обратно занимает 100 мс. TCP-соединение: 100 мс. TLS 1.2 добавляет два обмена: 2 × 100 = 200 мс, и первый запрос уходит на 300-й миллисекунде. TLS 1.3 добавляет один: 100 мс, запрос уходит на 200-й миллисекунде. На сотнях запросов от пользователей с разными серверами эта разница и есть причина, почему TLS 1.3 считают заметно быстрее.

Осторожно: «чем длиннее ключ, тем лучше» не так. RSA 2048 бит сегодня считается достаточным, а RSA 4096 медленнее без практической пользы для веб-сайта. И защита TLS не вечная: она держится на математике, и её перестают считать надёжной по мере роста мощностей компьютеров, поэтому версии и длины ключей периодически поднимают.

Прикинь сам: есть ли смысл записывать зашифрованный трафик, если надеешься украсть ключ сервера потом?

При прямой секретности нет: ключ сессии не выводится из долгоживущего ключа сервера, и старые записи не расшифровать.

Главное: используй TLS 1.2 и 1.3. Прямая секретность делает записанный трафик бесполезным после кражи ключа.

Проверь понимание: атакующий год записывал зашифрованный трафик к твоему сайту, а потом украл закрытый ключ сертификата. Прочитает ли он записи, если использовалась прямая секретность?

Ответ

Нет. Общий ключ каждого соединения получался обменом Диффи-Хеллмана с одноразовыми значениями, которых нет ни на диске, ни в записи. Закрытый ключ сертификата нужен только для подписи (доказательства личности), расшифровать им записи нельзя. Без прямой секретности, когда общий ключ шифровался открытым ключом сервера, украденный ключ открыл бы все старые записи.

Для стенда мы выпускали сертификат сами. Как это делает публичный центр сертификации и как он убеждается, что сайт твой, смотрим дальше.

Как публичный CA убеждается, что сайт твой (ACME и Let’s Encrypt)

CA подписывает сертификат «example.org принадлежит этому ключу». Значит, он обязан убедиться, что запрос прислал тот, кто управляет example.org, а не посторонний. Когда это делали вручную по почте и факсам, сертификаты стоили денег и продлевались раз в год. Сейчас проверка автоматическая, и это одна из причин, почему HTTPS стал повсеместным.

Нотариус просит тебя доказать, что квартира твоя: «повесь на дверь бумажку с моим номером, я приду и посмотрю». Если бумажка на месте, значит, дверь открывается именно тобой. Оговорка: нотариус проверяет, что ты можешь повесить бумажку, а не то, что ты честный человек.

Протокол называется ACME (Automatic Certificate Management Environment): им пользуются клиентские программы вроде certbot и cert-manager (программа, которая сама выпускает и продлевает сертификаты в Kubernetes: системе, запускающей приложения на многих машинах, урок 5.1; о cert-manager в уроке 9.4), а самый известный CA, работающий по нему, Let’s Encrypt (бесплатный, некоммерческий). Порядок:

  1. Клиент создаёт пару ключей и просит у CA сертификат для example.org.
  2. CA даёт задание (challenge): «докажи, что управляешь именем». Два основных варианта. HTTP-01: положить файл со случайным значением по адресу http://example.org/.well-known/acme-challenge/<токен>, CA сам его скачает. DNS-01: создать в DNS запись TXT с именем _acme-challenge.example.org и значением от CA (урок 2.3).
  3. CA проверяет и, если всё совпало, подписывает сертификат.
  4. Клиент устанавливает сертификат и настраивает продление, потому что срок короткий.
клиент (certbot)                         CA (Let's Encrypt)
   |-- «дай сертификат для example.org» ---->|
   |<-- «положи токен T по адресу /.well-known/acme-challenge/T» --|
   |-- кладёт файл на свой сайт              |
   |<-- CA заходит по http://example.org/... и читает файл -------|
   |<-- проверка пройдена: вот подписанный сертификат ------------|

Пример. Почему для notes.lab этот способ не работает: у CA нет способа зайти на http://notes.lab/, имени .lab не существует в публичном DNS. Поэтому в лабораториях и внутренних сетях используют свой CA (задание 3) или внутренние службы, а публичный CA годится только для имён, которые видны из интернета. Второй нюанс: сертификат для *.example.org (wildcard) выдаётся только через DNS-01, потому что HTTP-проверка про один конкретный адрес.

Осторожно: бесплатный сертификат не «хуже». Криптографически он такой же: разница в том, что тебя проверили только на владение именем, а не на юридическую личность организации. Для шифрования и защиты от подмены этого достаточно.

Прикинь сам: как публичный CA убеждается, что сайт именно твой?

Просит доказать управление: положить файл на сайт (HTTP-проверка) или создать запись в DNS (DNS-проверка).

Главное: ACME автоматизирует выпуск и продление, а wildcard подтверждают через DNS.

Проверь понимание: тебе нужен один сертификат на example.org и *.example.org. Какой способ подтверждения подойдёт и почему не подойдёт HTTP-01?

Ответ

DNS-01. Wildcard-имя покрывает бесконечное число поддоменов, поэтому CA хочет убедиться, что ты управляешь всей зоной DNS, а не одним адресом. Файл по HTTP доказывает контроль только над одним веб-сервером.

Сертификат выпущен и работает. Но что делать, если ключ утёк раньше, чем истёк срок? Об этом следующий раздел: отзыв и взаимный TLS.

Отзыв сертификатов и взаимный TLS

Что делать, если закрытый ключ утёк, а сертификат ещё действителен год? Ждать нельзя. Нужен способ объявить сертификат недействительным раньше срока (отзыв, revocation). И наоборот: иногда сервер хочет убедиться, кто именно к нему пришёл, а не любой клиент из интернета. Для этого клиент тоже предъявляет сертификат.

Как устроен отзыв. CA ведёт список отозванных сертификатов (CRL, Certificate Revocation List) или отвечает на запрос «жив ли сертификат с номером N» (протокол OCSP). Клиент может справиться об этом перед доверием. На практике проверка отзыва работает неровно: браузеры и библиотеки реализуют её по-разному, а curl с OpenSSL по умолчанию её не запрашивает. Поэтому индустрия идёт к другой защите: короткие сроки. Сертификат на 47-90 дней «отзывается» сам, а автоматическое продление делает такие сроки безболезненными (см. раздел про срок выше).

Как устроен взаимный TLS (mTLS). Обычный TLS проверяет только сервер. В mTLS (mutual TLS) сервер тоже просит сертификат у клиента, клиент отдаёт его вместе с подписью своего закрытого ключа, и сервер проверяет цепочку так же, как клиент проверял сервер. Аналогия: в хорошем офисе тебя не только пускают, потому что ты нашёл правильное здание (сертификат сервера), но и охрана на входе проверяет твой пропуск (сертификат клиента). В nginx для этого есть директивы ssl_client_certificate (каким CA доверять при проверке клиентов) и ssl_verify_client on;. Такая схема нужна, когда друг с другом говорят сервисы (в Kubernetes, где каждый сервис получает свой сертификат), а не люди с паролями.

Пример. Ты открываешь https://notes.lab/ в браузере: сертификат предъявляет только сервер, а браузер проверяет его; клиента сервер знает по логину и паролю (уровень выше, HTTP). Микросервис orders вызывает payments: payments требует сертификат клиента и разрешает доступ только тем, чей сертификат подписан внутренним CA компании, и при этом видит в сертификате имя orders.

Осторожно: «mTLS» и «TLS с паролем» не одно и то же. Пароль проверяется прикладным уровнем (HTTP) уже после TLS, а сертификат клиента проверяется в самом рукопожатии, до любого запроса.

Прикинь сам: ключ украден, а сертификат действует ещё 60 дней. Можно ли просто ждать?

Нет: до конца срока им можно пользоваться. Сертификат нужно отозвать и выпустить новый с новым ключом.

Главное: отзыв закрывает скомпрометированный сертификат до срока, а взаимный TLS проверяет и клиента.

Проверь понимание: у сервиса скомпрометирован закрытый ключ сертификата, действует ещё 60 дней. Назови два способа закрыть проблему.

Ответ

Первый: отозвать сертификат у CA (CRL/OCSP) и выпустить новый с новой парой ключей. Второй: если сертификат был выпущен на короткий срок и автоматически продляется, заменить ключ и сертификат и просто дождаться, пока старый истечёт. В обоих случаях нужна новая пара ключей: старый ключ считается скомпрометированным навсегда.

Остаётся последняя тонкость: какие именно имена и адреса сертификат покрывает и как клиент это сверяет.

Имена в сертификате: несколько имён, wildcard и IP

У одного сервиса бывает несколько адресов: notes.lab, www.notes.lab, адрес 192.168.64.5, тестовое stage.notes.lab. Выпускать на каждый отдельный сертификат неудобно, и в SAN можно перечислить всё сразу. Но правила сверки строгие, и на них легко споткнуться.

Охрана сверяет гостей по списку: в нём написаны имена, и охранник сверяет паспорт буква в букву. «Иван Петров» в списке не пропустит «Ивана Петровича». Оговорка: у охранника есть «сокращение» на вечеринку с любым человеком с одной фамилией, это wildcard, но оно работает по узким правилам.

Клиент берёт имя из адреса (то, что ты набрал: notes.lab, www.notes.lab или IP) и ищет его среди записей SAN. Записи бывают двух видов: DNS: для имён и IP: для адресов. Имя сверяется с DNS:, IP-адрес только с IP:. Регистр букв не важен. Звёздочка * в самой левой части имени заменяет ровно одну часть имени между точками, и нигде больше.

Пусть в SAN сертификата стоит DNS:notes.lab, DNS:*.notes.lab, IP:192.168.64.5. Сверим шесть адресов:

адрес в строке браузера       подходит?   почему
notes.lab                     да          точное совпадение с DNS:notes.lab
www.notes.lab                 да          звёздочка заменяет одну часть: www
api.stage.notes.lab           нет         звёздочка заменяет одну часть, а здесь две (api.stage)
NOTES.LAB                     да          регистр не важен
192.168.64.5                  да          есть IP:192.168.64.5
192.168.64.6                  нет         такого IP нет в списке

Без строки DNS:notes.lab (только *.notes.lab) голое имя notes.lab не подошло бы: звёздочка требует, чтобы слева было хоть что-то, поэтому в сертификат для домена обычно кладут оба варианта.

Осторожно: wildcard покрывает не все поддомены на любую глубину, а только один уровень. И имя в адресе и имя в DNS-записи не одно и то же: сверка идёт с тем, что набрано в адресной строке. Если ты открыл https://192.168.64.5/, а в SAN только DNS:notes.lab, будет ошибка имени, даже если этот IP и есть сервер notes.lab.

Прикинь сам: покрывает ли *.example.org адрес a.b.example.org?

Нет: звёздочка заменяет ровно один уровень имени.

Главное: в SAN можно перечислить несколько имён, wildcard закрывает один уровень, а IP добавляют отдельной записью.

Проверь понимание: в SAN стоит DNS:*.example.org. Подойдёт ли сертификат для example.org, для a.example.org и для a.b.example.org?

Ответ

Только для a.example.org. Голое example.org звёздочка не покрывает (слева нечего заменять), а a.b.example.org это две части слева, звёздочка заменяет одну.

Теперь у нас есть всё, чтобы чинить поломки. Нужен только порядок, в котором искать причину.

Порядок диагностики: как искать проблему с сертификатом

Ошибка HTTPS звучит одинаково пугающе, а причины лежат на трёх разных уровнях: сеть, рукопожатие, проверка сертификата. Если идти наугад, легко потратить час не там. Нужна схема: от нижнего уровня к верхнему, на каждом шаге один вопрос.

Врач сначала спрашивает «дышит ли пациент», потом «есть ли пульс», и только потом изучает анализы. Оговорка: в сети проверки дешёвые, и можно делать несколько подряд, а не по одной.

Идём по порядку, который совпадает с порядком событий при соединении:

flowchart TD
    P1{"1. Порт слушает?<br>ss -ltn, grep :443"} -->|"нет"| E1["nginx не запущен или нет listen 443<br>(уроки 2.2, 2.5)"]
    P1 -->|"да"| P2{"2. Рукопожатие проходит?<br>openssl s_client -servername"}
    P2 -->|"нет"| E2["Ошибка TLS: версия, шифры,<br>нет ssl в listen"]
    P2 -->|"да"| P3["3. Какой сертификат отдан?<br>openssl x509 -subject -issuer -dates"]
    P3 --> P4["4. Цепочка сходится?<br>код 0, 20, 21 или 18"]
    P4 --> P5["5. Срок?<br>-checkend 0, date -u"]
    P5 --> P6["6. Имя?<br>-verify_hostname, код 62"]

Первые два шага относятся к сети и рукопожатию, а сертификат разбирают только после них.

Первые два пункта относятся к сети и рукопожатию: если порт закрыт, curl напишет Failed to connect (это TCP, а не TLS). Если порт открыт, но по нему ходит обычный HTTP, curl сообщит wrong version number: nginx не был настроен на TLS. Пункты 3-6 это проверки сертификата, каждой из них соответствует своя строка справочника ниже.

Утром пишут: «notes.lab не открывается». Ты проходишь схему. Пункт 1: ss -ltn показывает :443, порт слушают. Пункт 2: s_client доходит до ответа, значит, сеть и рукопожатие в порядке. Пункт 3-5: -dates показывает notAfter=Sep 29 08:00:00 2026 GMT, а date -u печатает Sep 30 ...: срок вышел вчера. Виновата просрочка, а не сеть, не цепочка и не имя. На всё ушло две минуты, потому что каждый шаг исключал целый класс причин.

Осторожно: «ошибка сертификата значит, что что-то не так с сертификатом». Часто это неправда: при Connection refused вообще нет TLS, а при unable to get local issuer certificate сертификат может быть в идеальном порядке, а не хватает доверия у клиента. Поэтому сначала выясняй уровень, потом чини.

Прикинь сам: curl пишет Failed to connect. Это проблема сертификата?

Нет, это TCP: порт закрыт, до TLS дело не дошло. Начинай с первого шага диагностики.

Главное: проверяй по порядку: порт, рукопожатие, сертификат, цепочка, срок, имя.

Проверь понимание: curl пишет SSL routines:...:wrong version number. На каком шаге схемы проблема и что проверишь?

Ответ

На шаге 2: рукопожатие не начинается. Клиент говорит по TLS, а на порту отвечает не-TLS (например, обычный HTTP: в listen нет слова ssl, или это порт 80, к которому обратились по https://). Проверка: sudo nginx -T | grep -B2 -A2 'listen'.

Схема подсказывает, на каком уровне искать. Что значит конкретный текст ошибки, видно в справочнике.

Справочник: какая ошибка что значит

Из-за сертификатов ломается чаще всего одно и то же. Три первые строки таблицы это три сценария «Сломай и почини» ниже. Тексты curl бывают чуть разными в разных версиях: в Ubuntu 24.04 (curl 8.5.0) и в Ubuntu 26.04 (curl 8.18.0) они отличаются, оба варианта проверены.

Что видишь (curl 8.5.0, Ubuntu 24.04) Что в 26.04 (curl 8.18.0) Код openssl Что случилось Где искать
SSL certificate problem: self-signed certificate SSL certificate OpenSSL verify result: self-signed certificate (18) 18 Сертификат подписан сам собой, клиент ему не доверяет Нужный ли сертификат отдаёт nginx; есть ли он в доверенных у клиента
SSL certificate problem: certificate has expired SSL certificate OpenSSL verify result: certificate has expired (10) 10 Срок вышел (или часы клиента убежали) -dates, -checkend, date -u
SSL: no alternative certificate subject name matches target host name 'notes.lab' ... matches target hostname 'notes.lab' 62 В SAN нет имени из адреса -ext subjectAltName
SSL certificate problem: unable to get local issuer certificate по той же схеме, (20) 20 или 21 Цепочка не дошла до доверенного корня Отдаёт ли сервер промежуточные; есть ли корень у клиента
Failed to connect ... port 443 ... Couldn't connect to server Could not connect to server нет Порт 443 не слушают: проблема уровня TCP (урок 2.2), а не TLS ss -ltn, работает ли nginx

Обрати внимание: у трёх «TLS-ошибок» одна и та же общая форма curl: (60). Код 60 означает «сертификат не прошёл проверку», а что именно не так, написано после двоеточия. Читай текст, не код.

Ошибки nginx -t при настройке (cannot load certificate, key values mismatch, no "ssl_certificate" is defined) разобраны в заданиях как «Типичные ошибки».

Практика

Все задания идут на учебной ВМ из урока 1.8 с уже работающими «Заметками» и nginx из урока 2.5. Ты работаешь под обычным пользователем ubuntu, а где нужны права администратора, стоит sudo. Значения, которые меняются от запуска к запуску (даты, серийные номера, хеши), у тебя будут другими: сверяй форму вывода, а не цифры. Программа openssl (набор криптографических инструментов) уже стоит в Ubuntu. Если её нет, поставь: sudo apt install -y openssl.

Задание 1: посмотреть на настоящий сертификат и цепочку

Цель: увидеть глазами то, о чём шла речь в теории: издателя, срок, SAN и цепочку из нескольких звеньев. Нужен выход в интернет.

Сначала разберём команду. openssl s_client работает как curl для TLS: устанавливает соединение и печатает всё, что происходит в рукопожатии.

  • -connect example.com:443 куда подключаться (имя и порт);
  • -servername example.com какое имя послать в SNI. Без него сервер, у которого много сайтов, отдаст сертификат по умолчанию;
  • -showcerts печатать все сертификаты цепочки, а не только лист;
  • </dev/null подаёт на вход пустоту. Без этого s_client после рукопожатия будет ждать, что ты что-то напечатаешь, и повиснет (ты увидишь read R BLOCK);
  • 2>/dev/null выбрасывает служебный шум со стандартного потока ошибок (о потоках см. урок 1.2);
  • | grep -E '...' оставляет только нужные строки. Значок | (конвейер) передаёт вывод одной команды на вход следующей, -E включает расширенные регулярные выражения, где | внутри кавычек означает «или».
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
  | grep -E '^ *[0-9] s:|^ +i:|Verification|Verify return code'
 0 s:CN = example.com
   i:C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
 1 s:C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
   i:C = US, O = SSL Corporation, CN = SSL.com TLS Transit ECC CA R2
 2 s:C = US, O = SSL Corporation, CN = SSL.com TLS Transit ECC CA R2
   i:C = US, O = SSL Corporation, CN = SSL.com TLS ECC Root CA 2022
 3 s:C = US, O = SSL Corporation, CN = SSL.com TLS ECC Root CA 2022
   i:C = GB, ST = Greater Manchester, L = Salford, O = Comodo CA Limited, CN = AAA Certificate Services
Verification: OK
Verify return code: 0 (ok)

Как читать вывод: номера 0-3 это звенья цепочки, лист первый. s: (subject) кому выдан сертификат, i: (issuer) кто выдал. Проверь звенья по очереди: i: нулевого равен s: первого, i: первого равен s: второго и так далее: это и есть цепочка доверия из теории. Строки Verification: OK и Verify return code: 0 (ok) говорят, что клиент дошёл до корня из системного хранилища и все проверки прошли. Если у тебя другие имена издателей или другое число звеньев, это нормально: сайт мог сменить издателя.

Теперь посмотрим на сам лист. Здесь echo | подаёт пустую строку вместо </dev/null, а результат s_client целиком отправляется в openssl x509: эта команда читает сертификат из входа и печатает выбранные поля. -noout не печатает сам сертификат в base64, -subject -issuer -dates выводят названные поля, -ext subjectAltName выводит одно расширение.

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
subject=CN = example.com
issuer=C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
notBefore=Sep 26 22:49:11 2026 GMT
notAfter=Dec 25 22:56:35 2026 GMT
X509v3 Subject Alternative Name:
    DNS:example.com, DNS:*.example.com

Как читать вывод: notBefore и notAfter это границы срока. Посчитай сам: с 26 сентября по 25 декабря ровно 90 дней и 7 минут (4 дня до конца сентября, 31 в октябре, 30 в ноябре, 25 в декабре: 4 + 31 + 30 + 25 = 90; минуты набегают из-за времени), то есть срок 90 дней. DNS:*.example.com значит «любой поддомен первого уровня», например www.example.com. Обрати внимание: в SAN есть и example.com без звёздочки, потому что звёздочка голое имя не покрывает.

Типичные ошибки:

  • команда «зависла» после вывода цепочки: забыл </dev/null или echo |, s_client ждёт ввода. Нажми Ctrl+C и повтори;
  • Connection timed out или Name or service not known: нет выхода в интернет или не работает DNS (урок 2.3). Это не проблема TLS.

Проверь понимание: в выводе четыре звена, а сервер по правилам обязан прислать три. Какое звено лишнее и почему клиенту это не мешает?

Ответ

Нижнее, самое верхнее, AAA Certificate Services: это корень, он у клиента и так есть в хранилище, присылать его не обязательно. Лишнее звено не вредит: клиент всё равно сверяет с тем, что лежит у него.

Задание 2: выпустить самоподписанный сертификат для notes.lab

Цель: получить пару «закрытый ключ + сертификат» для notes.lab и проверить, что в SAN стоит нужное имя.

Каталог для файлов: /etc/notes/ уже есть с урока 1.8, внутри создаём tls. mkdir -p создаёт каталог (и родительские, если их нет) и не ругается, если он уже есть.

Разберём главную команду. openssl req создаёт запросы и сертификаты. Ключ -x509 говорит: не делать запрос на подпись, а сразу выпустить сертификат, подписав им самим себя.

  • -newkey rsa:2048 создать новую пару ключей RSA длиной 2048 бит;
  • -nodes не шифровать закрытый ключ паролем (иначе nginx при каждом запуске спрашивал бы пароль; слово от «no DES», историческое);
  • -days 365 срок действия;
  • -keyout и -out куда положить ключ и сертификат;
  • -subj "/CN=notes.lab" поле Subject; без него openssl задал бы вопросы в диалоге;
  • -addext "subjectAltName=DNS:notes.lab" добавить расширение SAN: то самое, по которому клиенты сверяют имя.
sudo mkdir -p /etc/notes/tls
sudo openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
  -keyout /etc/notes/tls/notes.key -out /etc/notes/tls/notes.crt \
  -subj "/CN=notes.lab" -addext "subjectAltName=DNS:notes.lab"
sudo chmod 600 /etc/notes/tls/notes.key
sudo chmod 644 /etc/notes/tls/notes.crt
ls -l /etc/notes/tls/

Пока ключ создаётся, openssl печатает на стандартный поток ошибок точки и плюсы (.....+++++): это индикатор поиска простых чисел для ключа, не ошибка. Итог ls -l:

-rw-r--r-- 1 root root 1143 Sep 30 11:52 notes.crt
-rw------- 1 root root 1704 Sep 30 11:52 notes.key

Как читать вывод: chmod 600 оставил на ключе права только владельцу-root (урок 1.3): -rw-------. Сертификат не секрет, ему 644 (читают все). Размеры файлов у тебя могут отличаться на несколько байт.

Теперь прочитаем сертификат. Разбор: openssl x509 -in <файл> читает сертификат из файла, флаги те же, что были в задании 1.

openssl x509 -in /etc/notes/tls/notes.crt -noout -subject -issuer -dates -ext subjectAltName
subject=CN = notes.lab
issuer=CN = notes.lab
notBefore=Sep 30 11:52:40 2026 GMT
notAfter=Sep 30 11:52:40 2027 GMT
X509v3 Subject Alternative Name:
    DNS:notes.lab

Как читать вывод: subject равен issuer: сертификат самоподписанный, как и должно быть. Срок ровно год от «сейчас». В SAN DNS:notes.lab. На Ubuntu 26.04 (OpenSSL 3.5) имена печатаются без пробелов вокруг знака равенства: CN=notes.lab, содержание то же.

Проверим ещё одно: что ключ и сертификат действительно составляют пару. Если их перепутать (например, скопировать ключ от другого сертификата), nginx откажется стартовать, и ошибка будет непонятной. Пара считается парой, когда открытый ключ из сертификата совпадает с открытым ключом, вычисленным из закрытого. Разбор: openssl x509 -pubkey достаёт открытый ключ из сертификата, openssl pkey -pubout вычисляет его из закрытого ключа, -outform der записывает в двоичном виде, а sha256sum превращает в короткий отпечаток для сравнения глазами.

openssl x509 -in /etc/notes/tls/notes.crt -noout -pubkey | openssl pkey -pubin -outform der | sha256sum
sudo openssl pkey -in /etc/notes/tls/notes.key -pubout -outform der | sha256sum
e9b020fc30b6e328a6b151d97935198ef0a305926a0ef19301a971edd873c0b4  -
e9b020fc30b6e328a6b151d97935198ef0a305926a0ef19301a971edd873c0b4  -

Как читать вывод: две строки должны совпасть символ в символ. Сами цифры у тебя будут другие, важно, что обе строки у тебя одинаковы. Это команда-диагностика: запомни её, она пригодится, когда nginx скажет key values mismatch.

Теперь эксперимент про SAN, которого нам не хватало в теории. Сделаем два лишних сертификата в каталоге /tmp и проверим, как реагирует openssl verify (команда, которая проверяет сертификат по цепочке; ключ -CAfile указывает, кому доверять, а -verify_hostname какое имя должно быть в сертификате). Ключи ты создаёшь как ubuntu, поэтому sudo не нужен.

cd /tmp
openssl req -x509 -newkey rsa:2048 -nodes -days 30 -keyout nosan.key -out nosan.crt -subj "/CN=notes.lab" 2>/dev/null
openssl req -x509 -newkey rsa:2048 -nodes -days 30 -keyout other.key -out other.crt -subj "/CN=notes.lab" \
  -addext "subjectAltName=DNS:www.notes.lab" 2>/dev/null
openssl verify -CAfile nosan.crt -verify_hostname notes.lab nosan.crt
openssl verify -CAfile other.crt -verify_hostname notes.lab other.crt
nosan.crt: OK
CN = notes.lab
error 62 at 0 depth lookup: hostname mismatch
error other.crt: verification failed

Как читать вывод: сертификат без SAN, но с CN=notes.lab утилита приняла (OK): она в этом случае смотрит в CN. Сертификат, где SAN есть, но в нём другое имя, отвергнут (hostname mismatch, код 62), хотя в CN правильное имя: раз SAN есть, CN не смотрят. Браузеры не примут и первый вариант. Вывод: всегда пиши имя в SAN. Убери временные файлы: rm -f /tmp/nosan.* /tmp/other.*.

Типичные ошибки:

  • Can't open "/etc/notes/tls/notes.key" for writing, Permission denied: забыл sudo перед openssl req;
  • openssl: command not found: sudo apt install -y openssl;
  • забыл -addext: сертификат выпустится без SAN, и это сломается в браузере (см. выше);
  • ключ читается всем: если оставить права по умолчанию, чужой пользователь сможет украсть ключ. Всегда chmod 600.

Проверь понимание: ты выпустил сертификат командой с -days 365. Через месяц ты поправил файл сертификата на сервере, заменив имя в SAN текстовым редактором. Пройдёт ли проверка подписи? Почему?

Ответ

Нет. Подпись считается от содержимого сертификата (как хеш в теории: изменил одну букву, и хеш другой). После правки подпись не совпадёт с содержимым, и клиент отвергнет сертификат. Изменить сертификат можно только выпустив новый.

Нейросеть может объяснить вывод openssl x509 и s_client построчно, но закрытые ключи ей вставлять нельзя: только сертификаты и тексты ошибок. Названную ею команду сначала запусти на тестовом сервере.

Задание 3: мини-CA, промежуточный сертификат и подпись руками

Цель: пройти путь, который делает любой центр сертификации: собственный корень, запрос на подпись, выпуск сертификата и проверка цепочки. Заодно подписать файл и увидеть, как ломается подпись при правке.

Работаем в отдельном каталоге, чтобы не мешать основным файлам. Все ключи здесь учебные.

mkdir -p ~/tls-lab && cd ~/tls-lab

Шаг 1. Корневой CA. Команда как в задании 2, но без -addext SAN, зато корню нужно расширение basicConstraints=CA:TRUE (этот сертификат вправе подписывать другие). Для req -x509 оно уже включено по умолчанию, но напишем явно:

openssl req -x509 -newkey rsa:2048 -nodes -days 365 -keyout ca.key -out ca.crt \
  -subj "/CN=Notes Lab Root CA" -addext "basicConstraints=critical,CA:TRUE" 2>/dev/null

Шаг 2. Ключ и запрос на подпись для сервера. Здесь новое: openssl req -new (без -x509) делает CSR, запрос на подпись. Закрытый ключ остаётся у тебя, в CSR уходит только открытый.

openssl req -newkey rsa:2048 -nodes -keyout leaf.key -out leaf.csr -subj "/CN=notes.lab" 2>/dev/null

Шаг 3. CA подписывает запрос. SAN нельзя доверять из CSR слепо, поэтому CA сам решает, какие расширения поставить. Пишем их в маленький файл (printf печатает строку, > кладёт её в файл) и передаём через -extfile.

Разбор openssl x509 -req: -in leaf.csr что подписываем, -CA ca.crt -CAkey ca.key кто подписывает (сертификат и закрытый ключ CA), -CAcreateserial создать файл с серийными номерами (ca.srl), -days 30 срок листа, -extfile leaf.ext какие расширения добавить.

printf 'subjectAltName=DNS:notes.lab\nbasicConstraints=CA:FALSE\n' > leaf.ext
openssl x509 -req -in leaf.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 30 -extfile leaf.ext -out leaf.crt
Certificate request self-signature ok
subject=CN = notes.lab

Как читать вывод: первая строка означает, что запрос не был подделан по дороге (он подписан ключом того, кто его создал). Вторая показывает, для какого имени выдан сертификат. На Ubuntu 24.04 вывод именно такой; на других версиях может быть чуть иначе.

Шаг 4. Проверка. Сначала без указания, кому доверять. Команда openssl verify сверяет только с системным хранилищем, где Notes Lab Root CA нет:

openssl verify leaf.crt
CN = notes.lab
error 20 at 0 depth lookup: unable to get local issuer certificate
error leaf.crt: verification failed

Как читать вывод: error 20 это тот самый код из справочника: издателя нет среди доверенных. depth 0 значит «на самом листе». Теперь скажем, что корню можно верить:

openssl verify -CAfile ca.crt leaf.crt
openssl verify -CAfile ca.crt -verify_hostname other.lab leaf.crt
leaf.crt: OK
CN = notes.lab
error 62 at 0 depth lookup: hostname mismatch
error leaf.crt: verification failed

Первая проверка проходит, вторая не проходит из-за имени: сертификат выпущен на notes.lab, а мы просим проверить как other.lab.

Шаг 5. Проверка срока. Как узнать, что будет через год, не дожидаясь его? Ключ -attime подставляет другое «сейчас» (число секунд с 1 января 1970, так называемое Unix-время; date -d '+400 days' +%s даёт его для момента через 400 дней). Учти, что -d работает в GNU-версии date, как в Ubuntu:

openssl verify -CAfile ca.crt -attime "$(date -d '+400 days' +%s)" leaf.crt
CN = Notes Lab Root CA
error 10 at 1 depth lookup: certificate has expired
CN = notes.lab
error 10 at 0 depth lookup: certificate has expired
error leaf.crt: verification failed

Как читать вывод: error 10 это «срок истёк». Проверка называет каждое просроченное звено: на глубине 1 корень (его 365 дней прошли раньше, чем через 400), на глубине 0 лист (его 30 дней тоже давно вышли). Одно просроченное звено обваливает всю цепочку, даже если остальные в порядке. Трюк с -attime пригодится в «Сломай и почини»: он показывает, просрочен ли сертификат на определённую дату.

Шаг 6. Три уровня. Добавим промежуточный CA между корнем и листом. Расширение pathlen:0 запрещает промежуточному выпускать других промежуточных (только листы), а keyUsage=critical,keyCertSign,cRLSign разрешает подписывать сертификаты:

openssl req -newkey rsa:2048 -nodes -keyout int.key -out int.csr -subj "/CN=Notes Lab Intermediate CA" 2>/dev/null
printf 'basicConstraints=critical,CA:TRUE,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign\n' > int.ext
openssl x509 -req -in int.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 30 -extfile int.ext -out int.crt 2>/dev/null
openssl x509 -req -in leaf.csr -CA int.crt -CAkey int.key -CAcreateserial -days 30 -extfile leaf.ext -out leaf.crt 2>/dev/null
openssl verify -CAfile ca.crt leaf.crt
openssl verify -CAfile ca.crt -untrusted int.crt leaf.crt
openssl verify -CAfile ca.crt -untrusted int.crt -show_chain leaf.crt

В команде выше лист переподписан промежуточным (-CA int.crt -CAkey int.key), поэтому старый leaf.crt перезаписан. Ключ -untrusted int.crt говорит: «вот промежуточный сертификат, пользуйся им для построения цепочки, но не считай его доверенным по умолчанию». Так ведёт себя клиент, которому сервер прислал промежуточный вместе с листом.

CN = notes.lab
error 20 at 0 depth lookup: unable to get local issuer certificate
error leaf.crt: verification failed
leaf.crt: OK
Chain:
depth=0: CN = notes.lab (untrusted)
depth=1: CN = Notes Lab Intermediate CA (untrusted)
depth=2: CN = Notes Lab Root CA

Как читать вывод: без -untrusted цепочка не строится (сервер «забыл» отдать промежуточный: авария из теории). С ним проходит и -show_chain печатает все три звена. Слово (untrusted) относится к способу получения, доверенным остаётся только корень. Обрати внимание, что depth растёт от листа (0) к корню (2).

Склеим fullchain.pem, файл, который потом отдаст nginx: лист, затем промежуточный (корень не нужен):

cat leaf.crt int.crt > fullchain.pem
grep -c 'BEGIN CERTIFICATE' fullchain.pem
2

Как читать вывод: grep -c считает строки, где встретилось BEGIN CERTIFICATE: значит, в файле два сертификата. Порядок важен: лист первым.

Шаг 7. Подпись руками. Подпишем файл закрытым ключом корня и проверим открытым. Разбор: openssl dgst -sha256 -sign ca.key -out doc.sig doc.txt считает SHA-256 от doc.txt и шифрует хеш ключом (это и есть подпись), x509 -pubkey достаёт открытый ключ из сертификата, dgst -verify проверяет.

echo "заметка №1" > doc.txt
openssl dgst -sha256 -sign ca.key -out doc.sig doc.txt
ls -l doc.sig
openssl x509 -in ca.crt -noout -pubkey > ca.pub
openssl dgst -sha256 -verify ca.pub -signature doc.sig doc.txt
echo "заметка №2" > doc.txt
openssl dgst -sha256 -verify ca.pub -signature doc.sig doc.txt
-rw-r--r-- 1 ubuntu ubuntu 256 Sep 30 12:05 doc.sig
Verified OK
20D00A9CFFFF0000:error:02000068:rsa routines:ossl_rsa_verify:bad signature:../crypto/rsa/rsa_sign.c:430:
20D00A9CFFFF0000:error:1C880004:Provider routines:rsa_verify:RSA lib:../providers/implementations/signature/rsa_sig.c:774:
Verification failure

Как читать вывод: подпись всегда 256 байт (размер ключа 2048 бит = 256 байт), сколько бы ни весил документ. После правки одного символа проверка падает (две строки с error: это технические подробности OpenSSL, важно слово bad signature и итог Verification failure; коды в начале строк у тебя будут другие): подпись привязана к точному содержимому. Так ломается сертификат, если его изменить.

Типичные ошибки:

  • Can't open leaf.ext for reading, No such file or directory: ты не в каталоге ~/tls-lab или пропустил printf. Без файла расширений в листе не будет SAN;
  • error 20 ... unable to get local issuer certificate при проверке трёхуровневой цепочки: забыл -untrusted int.crt;
  • Verification failure у подписи: файл изменился после подписи, или подписывал другой ключ.

Проверь понимание: в шаге 6 мы выпустили промежуточный на 30 дней, а корень живёт 365. Что станет с листом на 31-й день?

Ответ

Лист (тоже 30 дней) и промежуточный оба просрочены, проверка упадёт с certificate has expired. Срок каждого сертификата в цепочке проверяется отдельно, и срок листа не должен превышать срок выдавшего его.

Задание 4: цепочка, SNI и системное доверие на временном сервере

Цель: воспроизвести на nginx аварию «сервер не отдал промежуточный», увидеть SNI в действии и научить систему доверять мини-CA. Чтобы не трогать «Заметки», поднимем временный HTTPS на порту 8443.

Сначала ещё один сертификат для «сервера по умолчанию» и запись имени default.lab (напоминание: /etc/hosts это локальная таблица имён, урок 2.3). Затем временный конфиг. cat > файл <<EOF ... EOF пишет в файл всё, что ниже, до строки EOF (heredoc). Перед cat стоит sudo tee, потому что записать в /etc/nginx/conf.d/ может только root, а > выполняется от твоего имени. Без кавычек вокруг EOF оболочка подставляет $HOME до записи, и nginx получает готовые пути. Директива return 200 ... отвечает прямо из nginx, без приложения.

cd ~/tls-lab
openssl req -x509 -newkey rsa:2048 -nodes -days 30 -keyout default.key -out default.crt \
  -subj "/CN=default.lab" 2>/dev/null
echo "127.0.0.1 default.lab" | sudo tee -a /etc/hosts
sudo tee /etc/nginx/conf.d/tls-lab.conf >/dev/null <<EOF
server {
    listen 8443 ssl default_server;
    server_name default.lab;
    ssl_certificate     $HOME/tls-lab/default.crt;
    ssl_certificate_key $HOME/tls-lab/default.key;
    return 200 "default\n";
}
server {
    listen 8443 ssl;
    server_name notes.lab;
    ssl_certificate     $HOME/tls-lab/leaf.crt;
    ssl_certificate_key $HOME/tls-lab/leaf.key;
    return 200 "notes only leaf\n";
}
EOF
sudo nginx -t && sudo systemctl reload nginx

Здесь два server на одном порту 8443: nginx выбирает между ними по SNI. Второй сначала отдаёт только лист (leaf.crt), без промежуточного.

Файл конфигурации читает master-процесс от root, поэтому ключи в домашнем каталоге ему доступны. Проверим лист без цепочки. Флаг --cacert ca.crt говорит curl, чей корень считать доверенным, вместо системного списка. Флаг --resolve не нужен: имя notes.lab уже в /etc/hosts.

curl -sS --cacert ca.crt https://notes.lab:8443/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it.

Ту же поломку покажет s_client (-CAfile то же, что --cacert):

echo | openssl s_client -connect notes.lab:8443 -servername notes.lab -CAfile ca.crt 2>/dev/null \
  | grep -E 'Verification|Verify return code'
Verification error: unable to verify the first certificate
Verify return code: 21 (unable to verify the first certificate)

Как читать вывод: код 21 «нашёл только лист и не смог достроить цепочку до корня». Ты дал клиенту корень, но между листом и корнем стоит промежуточный, а сервер его не прислал. Исправляем: заменим leaf.crt на fullchain.pem в конфиге.

sudo sed -i 's#leaf.crt;#fullchain.pem;#; s#notes only leaf#notes chain#' /etc/nginx/conf.d/tls-lab.conf
sudo nginx -t && sudo systemctl reload nginx
curl -sS --cacert ca.crt https://notes.lab:8443/
echo | openssl s_client -connect notes.lab:8443 -servername notes.lab -CAfile ca.crt 2>/dev/null \
  | grep -E '^ *[0-9] s:|Verification'
notes chain
 0 s:CN = notes.lab
 1 s:CN = Notes Lab Intermediate CA
Verification: OK

Как читать вывод: сервер отдаёт два звена, клиент достраивает третье своим корнем, и всё сходится. sed -i правит файл на месте, s#старое#новое# заменяет текст (разделитель #, потому что в путях есть /).

SNI. Спросим тот же порт с разными именами и без имени вообще:

echo | openssl s_client -connect 127.0.0.1:8443 2>/dev/null | grep -E '^ *0 s:'
echo | openssl s_client -connect 127.0.0.1:8443 -servername notes.lab 2>/dev/null | grep -E '^ *0 s:'
echo | openssl s_client -connect 127.0.0.1:8443 -servername default.lab 2>/dev/null | grep -E '^ *0 s:'
 0 s:CN = default.lab
 0 s:CN = notes.lab
 0 s:CN = default.lab

Как читать вывод: порт один и тот же, а сертификат разный. Без -servername nginx не знает, чей сертификат нужен, и отдаёт сертификат default_server. Если обратиться к сайту по IP-адресу, curl тоже не передаёт SNI и получит сертификат по умолчанию. Так что https://192.168... и https://notes.lab для одного nginx это два разных случая.

Системное доверие. Сейчас curl без --cacert ошибётся: системе неизвестен Notes Lab Root CA.

curl -sS https://notes.lab:8443/
sudo cp ca.crt /usr/local/share/ca-certificates/notes-lab-ca.crt
sudo update-ca-certificates
curl -sS https://notes.lab:8443/
curl: (60) SSL certificate problem: unable to get local issuer certificate
...
rehash: warning: skipping ca-certificates.crt,it does not contain exactly one certificate or CRL
1 added, 0 removed; done.
notes chain

Как читать вывод: update-ca-certificates пересобрала общий список, вывод 1 added значит, что добавлен твой корень. Предупреждение rehash: warning безвредно. После этого curl без всяких ключей доверяет любому сертификату, подписанному этим корнем.

Теперь обязательная уборка: мини-CA нельзя оставлять в доверенных навсегда. Если его ключ (который лежит у тебя в домашнем каталоге) попадёт чужим, они смогут выдавать сертификаты для любого сайта, которым будет доверять твоя система.

sudo rm /usr/local/share/ca-certificates/notes-lab-ca.crt
sudo update-ca-certificates --fresh
sudo rm /etc/nginx/conf.d/tls-lab.conf
sudo sed -i '/default.lab/d' /etc/hosts
sudo nginx -t && sudo systemctl reload nginx
cd ~ && rm -rf ~/tls-lab
curl -sS https://notes.lab:8443/ ; echo "код: $?"

Ожидаемо --fresh пересоберёт всё хранилище (в выводе будет ... added, 0 removed, у тебя число другое), а последняя команда напомнит, что на порту 8443 больше никого нет (curl: (7) Failed to connect ... Couldn't connect to server).

Типичные ошибки:

  • nginx: [emerg] cannot load certificate ...: No such file or directory: неверный путь. $HOME подставился пустым, если команда запущена не под ubuntu, или файла нет;
  • curl: (35) ... wrong version number при обращении на 8443: в блоке забыли ssl в listen, nginx говорит на порту открытым HTTP;
  • update-ca-certificates ничего не добавила: имя файла не заканчивается на .crt;
  • после уборки curl по-прежнему доверяет: забыл --fresh или не удалил файл.

Проверь понимание: curl https://notes.lab:8443/ работает, а Python-скрипт на этой же машине падает с ошибкой сертификата. Назови две причины.

Ответ

Первая: у скрипта свой список доверенных (например, пакет certifi в библиотеке requests не читает общесистемное хранилище). Вторая: сертификат нужен другой цепочки, а скрипт подключается по другому имени или по IP, и в SAN его нет. Начинай с текста ошибки: он скажет, о чём именно речь.

Задание 5: HTTPS для «Заметок»

Цель: включить HTTPS для проекта и убедиться, что редирект и HSTS работают. Сертификат из задания 2 (/etc/notes/tls/notes.crt и notes.key) уже лежит на месте, app.py не меняется (v3).

Конфиг nginx хранится в репозитории проекта: ~/notes/deploy/nginx/notes.conf (так начали в уроке 2.5). Замени его содержимое на такое:

# Порт 80 теперь только отправляет всех на HTTPS
server {
    listen 80;
    server_name notes.lab;
    # 301 = «переехали навсегда»; путь и параметры сохраняем
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;                        # ssl: на этом порту говорим по TLS
    server_name notes.lab;

    ssl_certificate     /etc/notes/tls/notes.crt;   # сертификат (для цепочки здесь был бы fullchain)
    ssl_certificate_key /etc/notes/tls/notes.key;   # закрытый ключ, права 600
    ssl_protocols TLSv1.2 TLSv1.3;                  # старые версии не пускаем

    # HSTS: год «только https». always: добавлять и к ответам с ошибками
    add_header Strict-Transport-Security "max-age=31536000" always;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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;   # приложение узнает, что снаружи был https
    }
}

Разбор незнакомого: $host, $request_uri, $scheme, $remote_addr это переменные nginx (имя из запроса, путь с параметрами, схема http или https, адрес клиента). proxy_set_header добавляет заголовок в запрос, который уходит приложению: так приложение, стоящее за nginx, узнаёт настоящий адрес клиента и что снаружи был https (урок 2.5).

Копируем конфиг на место, проверяем и перезагружаем. Как в уроке 2.5: файл сайта лежит в sites-available, ссылка на него в sites-enabled уже есть.

sudo cp ~/notes/deploy/nginx/notes.conf /etc/nginx/sites-available/notes
sudo nginx -t
sudo systemctl reload nginx
ss -ltn | grep -E ':(80|443)\b'
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*
LISTEN 0      511          0.0.0.0:443       0.0.0.0:*

Как читать вывод: две строки nginx -t говорят, что синтаксис верен и файлы сертификата читаются: nginx при проверке действительно открывает файлы. Порт 443 теперь слушает (в самом ss заголовок может слипаться, это нормально). reload подхватывает конфиг без обрыва существующих соединений.

Проверим редирект. В команде curl -sI получаем только заголовки ответа (урок 2.4), а grep -i -E 'HTTP|location' оставляет статусную строку и заголовок Location (без -i регистр не игнорируется, а заголовки бывают в разном регистре).

curl -sI http://notes.lab/ | grep -i -E 'HTTP|location'
HTTP/1.1 301 Moved Permanently
Location: https://notes.lab/

Если сразу после reload ответ оказался 200 OK, а не 301, подожди секунду и повтори: старый рабочий процесс nginx ещё доживает и может ответить по-старому.

Теперь HTTPS. Без доверия к сертификату curl откажет:

curl -sS https://notes.lab/healthz
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html
...

Как читать вывод: это первая строка из справочника. Самоподписанному сертификату верить нечему, но мы-то знаем, что он наш. Скажем клиенту: доверяй именно этому файлу (--cacert).

curl -sS --cacert /etc/notes/tls/notes.crt https://notes.lab/healthz
curl -sSI --cacert /etc/notes/tls/notes.crt https://notes.lab/healthz | grep -i -E 'HTTP|strict'
curl -sS --cacert /etc/notes/tls/notes.crt https://notes.lab/headers
ok
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000
{"Host": "notes.lab", "X-Real-IP": "127.0.0.1", "X-Forwarded-For": "127.0.0.1", "X-Forwarded-Proto": "https", "Connection": "close", "User-Agent": "curl/8.5.0", "Accept": "*/*"}

Как читать вывод: ok это ответ /healthz из приложения, прошедший через TLS. Заголовок HSTS приходит только с HTTPS-ответом. Эндпоинт /headers (он появился в уроке 2.4) показывает, что увидело приложение: X-Forwarded-Proto: https (nginx честно сообщил про схему) и адрес клиента 127.0.0.1, потому что curl запущен на этой же машине. Connection: close и то, что запросы идут по HTTP/1.0, объясняются тем, что nginx по умолчанию так ходит к приложению. User-Agent у тебя будет свой.

Убедимся, что клиент действительно проверил сертификат и имя. Для этого -v и фильтр строк:

curl -sv --cacert /etc/notes/tls/notes.crt https://notes.lab/healthz 2>&1 | grep -E 'subjectAltName|SSL certificate verify'
*  subjectAltName: host "notes.lab" matched cert's "notes.lab"
*  SSL certificate verify ok.

Как читать вывод: обе проверки прошли: имя найдено в SAN, цепочка сошлась. Осталось проверить срок командой, которую ставят в мониторинг. -checkend N выходит с кодом 0, если сертификат проживёт ещё N секунд, и с кодом 1, если нет; $? хранит код выхода последней команды (урок 1.2). 30 дней это 30 × 86400 = 2 592 000 секунд.

openssl x509 -in /etc/notes/tls/notes.crt -noout -checkend 2592000; echo "код: $?"
openssl x509 -in /etc/notes/tls/notes.crt -noout -checkend $((400*86400)); echo "код: $?"
Certificate will not expire
код: 0
Certificate will expire
код: 1

Как читать вывод: до конца срока 365 дней, поэтому запас в 30 дней есть, а 400 дней нет. Так -checkend превращается в проверку для cron или мониторинга: код 1 значит «пора продлевать».

Что увидит перехватчик. Запишем трафик программой tcpdump (она копирует пакеты с сетевого интерфейса; ей нужен root). Записываем два участка одновременно: на порту 443 (снаружи, между клиентом и nginx) и на порту 8080 (между nginx и приложением). Интерфейс lo это loopback: клиент и сервер на одной машине. Разбор: -i lo интерфейс, -nn не переводить адреса и порты в имена, -s0 писать пакеты целиком, -w записать в файл, 'tcp port 443' фильтр, timeout 6 остановит запись через 6 секунд, & запускает команду в фоне, wait ждёт конца фоновых.

sudo apt install -y tcpdump
sudo timeout 6 tcpdump -i lo -nn -s0 -w /tmp/cap-443.pcap 'tcp port 443' 2>/dev/null &
sudo timeout 6 tcpdump -i lo -nn -s0 -w /tmp/cap-8080.pcap 'tcp port 8080' 2>/dev/null &
sleep 2
curl -s -w '\n' --cacert /etc/notes/tls/notes.crt -X POST -d '{"text":"пароль: hunter2"}' https://notes.lab/notes
wait
grep -a -c hunter2 /tmp/cap-443.pcap
grep -a -c hunter2 /tmp/cap-8080.pcap
grep -a -c notes.lab /tmp/cap-443.pcap
{"id": 1}
0
1
1

Как читать вывод: {"id": 1} это ответ приложения (запись создана; -w '\n' добавляет перевод строки, иначе следующее число прилипнет к ответу). grep -a читает двоичный файл как текст, -c считает совпавшие строки. В записи порта 443 пароля нет (0), потому что он зашифрован. На участке 8080, где nginx передаёт запрос приложению уже без шифрования, пароль виден. Это и есть терминация TLS на деле: защита кончается на nginx, поэтому участок за ним должен быть внутри доверенной машины или сети. Имя notes.lab в записи 443 есть: это SNI в ClientHello открытым текстом. Номер id у тебя может отличаться, если ты уже создавал заметки. Уберём за собой: sudo rm -f /tmp/cap-443.pcap /tmp/cap-8080.pcap. Файл записи создал root, поэтому нужен sudo.

Закрепи изменения в репозитории проекта: конфиг лежит в ~/notes/deploy/nginx/notes.conf, коммитим, как в теме про Git (ключи и сертификаты в git не кладём, они остаются в /etc/notes/tls/).

cd ~/notes && git add deploy/nginx/notes.conf && git commit -m "nginx: HTTPS для notes.lab, редирект и HSTS"

Типичные ошибки:

  • nginx: [emerg] ... cannot load certificate "/etc/notes/tls/notes.crt": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory ...): нет файла или неверный путь. Сначала выпусти сертификат (задание 2);
  • nginx: [emerg] ... SSL_CTX_use_PrivateKey("/etc/notes/tls/notes.key") failed (SSL: error:05800074:x509 certificate routines::key values mismatch): ключ не от этого сертификата. Сравни отпечатки открытых ключей, как в задании 2;
  • nginx: [emerg] no "ssl_certificate" is defined for the "listen ... ssl" directive in /etc/nginx/sites-enabled/notes:9: в блоке с listen 443 ssl забыли ssl_certificate;
  • curl: (7) Failed to connect to notes.lab port 443 ... Couldn't connect to server: nginx не запущен или конфиг не применён: sudo systemctl status nginx, ss -ltn. Это уже не TLS (урок 2.2);
  • Permission denied при чтении ключа: nginx читает конфиг от root, но если поставил владельцем другого пользователя без прав, возвращай sudo chown root:root и chmod 600.

Проверь понимание: пользователь набрал http://notes.lab/notes?page=2. Куда его перенаправит nginx и почему вторая часть адреса не потеряется?

Ответ

На https://notes.lab/notes?page=2. В директиве return 301 https://$host$request_uri; переменная $host даёт имя, $request_uri путь вместе с параметрами (/notes?page=2), они склеиваются после https://.

Сломай и почини

Теперь ты сломаешь HTTPS у «Заметок» тремя способами, которые в жизни ломают сервисы чаще всего, и найдёшь причину по тексту ошибки, не подглядывая в скрипт. Для этого нужен работающий HTTPS из заданий 2 и 5.

Скрипт поломки скачивается и запускается одинаково во всех уроках:

curl -fsSL -o /tmp/break-2.6.sh https://raw.githubusercontent.com/distinguished-sre/learning/main/devops/project/notes/break/2.6/break.sh
sudo bash /tmp/break-2.6.sh 1

Разбор: curl -f не сохраняет страницу с ошибкой, если сервер ответил кодом 4xx или 5xx, -s убирает индикатор, -S всё же показывает ошибку, -L идёт по редиректам, -o сохраняет в файл. Цифра после имени скрипта это номер сценария, слово fix возвращает всё как было. Скрипт проверит, что HTTPS настроен, и если нет, скажет, какое задание сделать. Он меняет только файлы сертификата и конфиг сайта «Заметок», выполняется от root (sudo).

Сценарии поломки: 1, 2, 3. Выбирай по очереди, не подсматривая, что именно сломано: сначала fix, потом следующий номер. Проверка, что всё работает (должна печатать ok):

curl -sS --cacert /etc/notes/tls/notes.crt https://notes.lab/healthz

Симптом

После запуска поломки ты повторяешь проверку и получаешь одну из трёх ошибок:

  • Сценарий 1: curl: (60) SSL certificate problem: self-signed certificate. Ты по-прежнему доверяешь файлу /etc/notes/tls/notes.crt, а сервер отдаёт что-то другое. (На Ubuntu 26.04 текст такой: SSL certificate OpenSSL verify result: self-signed certificate (18).)
  • Сценарий 2: curl: (60) SSL certificate problem: certificate has expired. (На 26.04: ... certificate has expired (10).)
  • Сценарий 3: curl: (60) SSL: no alternative certificate subject name matches target host name 'notes.lab'. (На 26.04: target hostname.)

Приложение при этом работает: curl -k вернёт ok (флаг -k мы используем только чтобы убедиться, что дело не в приложении). Сломан только TLS.

Гипотезы

Что может быть причиной для каждой ошибки? Выпиши свои варианты, потом проверь.

  • Ошибка про self-signed: nginx отдаёт не тот сертификат, что лежит по пути, которому ты доверяешь (конфиг смотрит на другой файл; nginx не перечитал конфиг; файл заменили, а reload не делали).
  • Ошибка про expired: сертификат действительно просрочен или часы на клиенте убежали вперёд.
  • Ошибка про no alternative certificate subject name: в SAN нет имени notes.lab (сертификат выпущен на другое имя, или имя из адреса не то).

Проверки

Идём от простого к сложному и всегда сравниваем то, что отдаёт сервер сейчас, с тем, что лежит на диске. Во многих поломках эти две вещи расходятся.

1. Какой сертификат реально отдаётся и что о нём думает клиент.

echo | openssl s_client -connect notes.lab:443 -servername notes.lab \
  -CAfile /etc/notes/tls/notes.crt -verify_hostname notes.lab 2>/dev/null | grep -E 'Verif|Verify return code'

Как читать вывод: здесь ты увидишь код из справочника: 18 (self-signed certificate), 10 (certificate has expired) или 62 (hostname mismatch). Обрати внимание на -verify_hostname: без него s_client сообщит Verification: OK даже при неверном имени, потому что сверка имени по умолчанию не включена.

2. Отпечаток того, что отдаёт сервер, против отпечатка файла на диске. Отпечаток (fingerprint) это хеш всего сертификата. Разбор: openssl x509 -noout -fingerprint -sha256 печатает его, а echo | openssl s_client достаёт сертификат из живого соединения.

echo | openssl s_client -connect notes.lab:443 -servername notes.lab 2>/dev/null | openssl x509 -noout -fingerprint -sha256
openssl x509 -in /etc/notes/tls/notes.crt -noout -fingerprint -sha256

Как читать вывод: если отпечатки разные, nginx отдаёт не тот файл, который ты проверяешь. Дальше ищи, откуда он его берёт.

3. Что записано в конфиге, который nginx реально использует. nginx -T печатает всю действующую конфигурацию (ключ -T от «test и dump»), 2>/dev/null убирает служебные сообщения, grep оставляет пути.

sudo nginx -T 2>/dev/null | grep ssl_certificate

Как читать вывод: в норме пути ведут в /etc/notes/tls/. Если видишь другой каталог, конфиг подменён. Сравнить с репозиторием проекта можно так: diff ~/notes/deploy/nginx/notes.conf /etc/nginx/sites-available/notes (строки с < из репозитория, с > с сервера).

4. Срок и имя в самом файле.

openssl x509 -in /etc/notes/tls/notes.crt -noout -dates -ext subjectAltName
openssl x509 -in /etc/notes/tls/notes.crt -noout -checkend 0; echo "код: $?"
date -u

Как читать вывод: сверь notBefore и notAfter с date -u (текущее время в UTC, то есть по Гринвичу: сертификаты хранят время именно в нём). -checkend 0 отвечает на вопрос «просрочен ли сертификат прямо сейчас»: код 1 и текст Certificate will expire значит да. Если срок в порядке, смотри строку X509v3 Subject Alternative Name и ищи в ней DNS:notes.lab.

5. Что будет в определённую дату. Если сертификат уже просрочен, узнать, когда именно, можно так же, как в задании 3 (-attime):

openssl verify -attime "$(date -d '+400 days' +%s)" -CAfile /etc/notes/tls/notes.crt /etc/notes/tls/notes.crt
CN = notes.lab
error 10 at 0 depth lookup: certificate has expired
error /etc/notes/tls/notes.crt: verification failed

Так проверяют, что сертификат «доживёт» до нужной даты, или наоборот, объясняют, почему он не проходит на машине с уехавшими часами.

Разбор

Открой только после того, как нашёл причину сам.

Сценарий 1: nginx отдаёт другой самоподписанный сертификат

Скрипт выпустил новый самоподписанный сертификат в каталог /etc/notes/tls-new/ и переписал в конфиге пути ssl_certificate на него. Файл /etc/notes/tls/notes.crt, которому ты доверяешь, остался нетронутым, поэтому вывод -checkend и -dates на нём выглядит идеально. Ошибка в другом: клиент получает сертификат, которого нет среди доверенных, а сертификат самоподписанный, значит self-signed certificate (код 18).

Как найти: отпечатки в проверке 2 расходятся, а nginx -T | grep ssl_certificate показывает /etc/notes/tls-new/. Чинится возвратом путей в конфиге и reload. Урок: проверять надо то, что отдаёт сервер, а не то, что лежит на диске, и reload нужен после любой замены файлов.

Сценарий 2: сертификат просрочен

Скрипт подменил сертификат на просроченный (тот же ключ и то же имя, но срок уже вышел). Ошибка certificate has expired (код 10) и -checkend 0 с кодом 1 это подтверждают. Реальная причина в жизни: не сработало автопродление. Поэтому такие сертификаты ставят на мониторинг за 14-30 дней до конца (команда -checkend из задания 5).

Если файл в порядке, а ошибка всё равно про срок, сверь date -u на клиенте: уехавшие часы тоже дают expired (или not yet valid, если часы отстали).

Сценарий 3: в SAN нет имени notes.lab

Скрипт выпустил сертификат, где в SAN стоит DNS:www.notes.lab, а notes.lab нет. Срок и подпись в порядке, поэтому s_client без -verify_hostname скажет Verification: OK, и на этом легко потерять время. С -verify_hostname notes.lab появится hostname mismatch (код 62), а curl пишет no alternative certificate subject name matches target host name 'notes.lab'. Проверка 4 показывает содержимое SAN.

Лечится выпуском сертификата с правильным SAN, как в задании 2. Урок: проверять нужно и цепочку, и срок, и имя (три проверки из теории).

Верни всё как было: sudo bash /tmp/break-2.6.sh fix. Команда безопасна при повторном запуске, а в конце сама печатает результат проверки (ok). После неё сотри скрипт: rm /tmp/break-2.6.sh.

ИИ в помощь

Нейросеть хорошо расшифровывает ошибки сертификатов, но может посоветовать «отключить проверку» (curl -k). Общие правила работы с ней: ИИ-помощник. Закрытые ключи (.key) ей не показывай никогда.

Задача: разобрать сертификат.

Вот вывод openssl x509 -noout -subject -issuer -dates -ext subjectAltName для моего сертификата:
<вставь вывод>.
Объясни по строкам: кому выдан, кто выдал, на какие имена действует, когда истекает.
Скажи, подойдёт ли он для адреса https://notes.lab/.

Проверь ответ: сверь имя со списком SAN и сравни даты с date -u. Типичная ошибка: нейросеть смотрит на CN и не замечает, что в SAN нужного имени нет.

Задача: найти причину ошибки TLS.

curl пишет: <вставь точный текст ошибки>.
Вот вывод echo | openssl s_client -connect <хост>:443 -servername <хост>: <вставь вывод без ключей>.
Перечисли причины от самой вероятной и для каждой команду проверки. Ничего не отключай, только предлагай проверки.

Проверь ответ: пройди по порядку из раздела о диагностике: порт, рукопожатие, сертификат, цепочка, срок, имя. Типичная ошибка: советует curl -k или verify=False, а это отключает защиту, а не чинит причину.

Задача: проверить конфиг HTTPS в nginx.

Вот блок server с ssl_certificate, ssl_certificate_key, ssl_protocols и редиректом с порта 80:
<вставь фрагмент без путей к секретам, если они чувствительны>.
Найди ошибки и слабые места, для каждого скажи, как проверить командой.

Проверь ответ: прогони sudo nginx -t и проверь curl -v с тестовой машины. Типичная ошибка: предлагает включить устаревшие версии TLS «для совместимости».

Словарик урока

Термин Простыми словами
TLS Слой защиты между TCP и HTTP: шифрует трафик, защищает от подмены и проверяет личность сервера
Токен Длинная секретная строка, которая работает как пароль для программ
Ключ (шифрования) Длинное секретное число, которое управляет шифрованием и расшифровкой
Рукопожатие (handshake) Обмен сообщениями в начале соединения: договориться о шифровании и проверить сервер
HSTS Заголовок: сервер просит браузер ходить к нему только по HTTPS
Дежурный Инженер, отвечающий в свою смену за работу сервисов
tcpdump Программа, которая показывает пакеты, проходящие через сетевой интерфейс
SSL Старое название TLS. В названиях осталось (ssl_certificate), сами версии SSL давно небезопасны
HTTPS HTTP внутри TLS, порт по умолчанию 443
Ключ (key) Длинное секретное число, управляющее шифрованием
Симметричное шифрование Один ключ и для запирания, и для отпирания. Быстро, но ключ надо как-то передать
Асимметричное шифрование Пара ключей: открытый и закрытый. Запертое одним отпирается только другим. Медленно
Открытый ключ (public key) Половина пары, которую можно показывать всем
Закрытый ключ (private key) Секретная половина пары, лежит только у владельца, права 600
Хеш (hash) Короткий «отпечаток» данных: одинаковые данные дают одинаковый хеш, любое изменение даёт совсем другой
SHA-256 Алгоритм хеширования, выдаёт 64 шестнадцатеричных символа
Подпись Хеш документа, зашифрованный закрытым ключом. Проверяется открытым
Рукопожатие (handshake) Обмен сообщениями в начале TLS-соединения, где стороны договариваются о ключе и проверяют сертификат
Диффи-Хеллман Способ договориться об общем ключе по открытой линии, не передавая его
SNI Имя сервера, которое клиент открытым текстом называет в начале рукопожатия
Сертификат (cert) Документ: имя, открытый ключ, срок, издатель и подпись издателя. Формат X.509
X.509 Стандарт формата сертификата
PEM Текстовый формат файла: двоичные данные в base64 между строками BEGIN и END
base64 Способ записать любые байты буквами и цифрами, чтобы файл можно было открыть как текст
Subject «Кому выдан» сертификат
Issuer «Кто выдал» сертификат
CN (Common Name) Устаревшее поле с именем в Subject. Браузеры его игнорируют
SAN (Subject Alternative Name) Список имён и IP, на которые действует сертификат. Проверяется в первую очередь
Wildcard Имя со звёздочкой (*.example.com): любой поддомен одного уровня
Not Before / Not After Границы срока действия сертификата
CA (Certificate Authority) Центр сертификации: организация, подписывающая сертификаты
Корневой CA (root) Тот, кому клиент доверяет заранее. Его сертификат самоподписанный
Промежуточный CA (intermediate) CA, подписанный корнем, который ежедневно подписывает сертификаты сайтов
Лист (leaf) Конечный сертификат сервера
Цепочка доверия Лист, подписанный промежуточным, подписанный корнем, которому клиент верит
fullchain Файл с листом и промежуточными, склеенными подряд. Корень не нужен
Хранилище доверенных (trust store) Список корней, которым доверяет система. В Ubuntu /etc/ssl/certs/
Самоподписанный сертификат Подписан своим же ключом: издатель равен субъекту
CSR Запрос на подпись: открытый ключ и имя без закрытого ключа
Перекрёстная подпись (cross-signing) Когда новый корень подписан старым, чтобы старые клиенты ему верили
Терминация TLS TLS начинается на клиенте и заканчивается на nginx, дальше запрос идёт по HTTP
HSTS Заголовок, по которому браузер запоминает: сайт только по HTTPS
Отпечаток (fingerprint) Хеш всего сертификата, для сравнения двух сертификатов
openssl Набор команд для ключей, сертификатов и проверки TLS
s_client Подкоманда openssl, работающая как curl для TLS

Вопросы с собеседований

Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.

1. [middle] [часто] Как проходит рукопожатие TLS?

Ответ

Клиент шлёт ClientHello: поддерживаемые версии, наборы шифров, имя хоста (SNI) и свою половину обмена ключами. Сервер отвечает ServerHello со своей половиной, и в TLS 1.3 обе стороны уже на этом шаге получают общий секретный ключ (обычно через ECDHE). Дальше сервер присылает сертификат (в TLS 1.3 он уже идёт зашифрованным) и подпись, клиент проверяет цепочку до доверенного корня, срок действия и совпадение имени. Только после этой проверки идут данные, симметричным шифрованием. Асимметричная криптография нужна для подтверждения подлинности сервера и обмена ключами, симметричная быстрее и несёт основной трафик. Проверяю вручную: openssl s_client -connect host:443 -servername host -verify_hostname host -verify_return_error. Одно -servername только задаёт SNI и не проверяет имя, а без -verify_return_error ошибка доверия не прерывает соединение.

Что хотят услышать: ClientHello и ServerHello, проверка сертификата клиентом, согласование общего ключа, затем симметричное шифрование, SNI, TLS 1.3

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

2. [junior] [часто] Чем симметричное шифрование отличается от асимметричного и зачем TLS использует оба?

Ответ

В симметричном один ключ на обе стороны: быстро, но ключ надо передать. В асимметричном пара (открытый и закрытый ключ): передавать секрет не нужно, но медленно. В TLS асимметричная часть (и обмен по Диффи-Хеллману) нужна, чтобы договориться об общем ключе и подтвердить личность, а данные потом шифруются быстрым симметричным ключом.

Что хотят услышать: «асимметричным договариваются, симметричным шифруют данные» и причина: скорость.

Красный флаг: «данные шифруются открытым ключом сервера».

3. [junior] [часто] Что такое цепочка доверия и как клиент решает, что сертификату можно верить?

Ответ

Сертификат сайта (лист) подписан промежуточным CA, тот подписан корневым. Клиент идёт снизу вверх: проверяет подпись каждого звена, срок каждого, что цепочка приходит в корень из его хранилища доверенных, и что имя из адреса есть в SAN листа. Если хоть что-то не так, соединение рвётся.

Что хотят услышать: четыре проверки: подпись, срок, корень в хранилище, имя.

Красный флаг: думать, что достаточно «сертификат подписан кем-то».

4. [junior] [на скорость] Что даёт TLS и чем HTTPS отличается от HTTP?

Ответ

HTTPS это тот же HTTP, только внутри TLS. TLS даёт три вещи: конфиденциальность (трафик зашифрован), целостность (подмену заметят) и аутентификацию (клиент проверяет, что сервер тот, за кого себя выдаёт). Порт по умолчанию 443, у HTTP 80.

Что хотят услышать: все три свойства, а не только «шифрует».

Красный флаг: «замочек значит сайт безопасен и честен». Он значит лишь, что канал защищён и имя подтверждено.

5. [junior] [на скорость] Что такое сертификат и что в нём лежит?

Ответ

Документ, который связывает имена с открытым ключом. В нём Subject (кому), Issuer (кто выдал), срок (Not Before, Not After), открытый ключ, список имён SAN и подпись издателя. Закрытого ключа в нём нет, сертификат не секретен: сервер отдаёт его каждому клиенту.

Что хотят услышать: назвать SAN, срок и подпись; сказать, что закрытого ключа в сертификате нет.

Красный флаг: «сертификат нужно хранить в секрете» или «в сертификате лежит закрытый ключ».

6. [middle] Клиент пишет unable to get local issuer certificate, а в браузере тот же сайт открывается. Причина?

Ответ

Сервер, скорее всего, отдаёт только лист, без промежуточных сертификатов. Браузер умеет докачать недостающий промежуточный (из кэша или по адресу, записанному в сертификате), поэтому «работает». curl, скрипты и мобильные приложения не докачивают и падают. Проверка: openssl s_client -connect host:443 -servername host -showcerts покажет, сколько сертификатов прислал сервер. Лечится подачей fullchain.pem (лист + промежуточные) в ssl_certificate.

Что хотят услышать: «не отдан промежуточный», проверка s_client и fullchain.

Красный флаг: «добавим -k»: это отключает проверку целиком.

7. [middle] Чем SAN отличается от CN и что произойдёт, если имени нет в SAN?

Ответ

SAN это список имён, на которые действует сертификат. CN устаревшее поле в Subject. Браузеры и современные клиенты сверяют адрес только с SAN и CN игнорируют, поэтому имени нет в SAN значит ошибка no alternative certificate subject name matches или hostname mismatch. Некоторые утилиты (в нашей проверке curl 8.5 и openssl verify) в отсутствие SAN ещё смотрят в CN, но полагаться на это нельзя: имя всегда пишут в SAN.

Что хотят услышать: сверка по SAN, CN устарел, wildcard и IP тоже кладут в SAN.

Красный флаг: «в CN написано правильно, значит подойдёт».

8. [middle] Что такое SNI и что случится, если клиент его не передал?

Ответ

SNI это имя сервера, которое клиент называет в первом сообщении рукопожатия (открытым текстом). Оно нужно, чтобы один IP и порт обслуживали много сайтов с разными сертификатами: сервер выбирает сертификат до расшифровки HTTP-заголовков. Без SNI (например, обращение по IP или s_client без -servername) nginx отдаст сертификат по умолчанию (default_server или первого блока), и это часто не тот, который ждёшь.

Что хотят услышать: сертификат выбирается по SNI, без него по умолчанию; имя в SNI видно перехватчику.

Красный флаг: «выбор сертификата идёт по заголовку Host» (он ещё зашифрован).

9. [middle] Как настроить HTTPS в nginx и что такое терминация TLS?

Ответ

В блоке server пишут listen 443 ssl;, ssl_certificate (лист или fullchain), ssl_certificate_key (закрытый ключ) и ssl_protocols TLSv1.2 TLSv1.3;. Отдельный блок на порту 80 делает return 301 https://$host$request_uri;. Терминация TLS означает, что TLS заканчивается на nginx: клиент говорит с ним по HTTPS, а приложению nginx передаёт запрос обычным HTTP по loopback. Приложению не нужно уметь шифровать, но участок nginx → приложение остаётся открытым, поэтому он должен быть внутри доверенной машины или сети.

Что хотят услышать: директивы, редирект, ключ 600, что участок за nginx не шифруется.

Красный флаг: приложение слушает 0.0.0.0 и позволяет обойти TLS.

10. [middle] [на скорость] Что такое HSTS и в чём его риск?

Ответ

Заголовок Strict-Transport-Security: max-age=...: браузер запоминает, что сайт ходит только по HTTPS, и сам переписывает http:// в https://, не делая открытого запроса. Это закрывает окно атаки на первом запросе. Риск: пока запись действует, браузер не даёт нажать «всё равно продолжить» при ошибке сертификата. Просроченный сертификат при большом max-age делает сайт недоступным. Поэтому начинают с малого значения и растят.

Что хотят услышать: зачем нужен, чем опасен, постепенный рост max-age.

Красный флаг: сразу ставить год и не следить за продлением сертификата.

11. [middle] Сертификат сервиса истёк ночью. Как ты найдёшь причину и что настроишь, чтобы не повторилось?

Ответ

Диагностика: openssl s_client -connect host:443 -servername host и openssl x509 -noout -dates покажут срок того, что реально отдаётся; сравниваю с файлом на диске (не забыли ли reload) и с date -u. Чиню выпуском нового сертификата и reload. Профилактика: автоматическое продление (например, cert-manager, см. урок 9.4) и мониторинг срока (-checkend за 14-30 дней) с алертом.

Что хотят услышать: сравнение «отдаётся» и «лежит на диске», автоматизация и алерт заранее.

Красный флаг: продлевать руками раз в год «по календарю».

12. [middle] Как проверить сертификат сервера из командной строки, не открывая браузер?

Ответ

Смотрю, что реально отдаёт сервер: openssl s_client -connect host:443 -servername host </dev/null. Флаг -servername передаёт SNI, без него можно получить не тот сертификат. Чтобы увидеть сроки и имена, передаю вывод в openssl x509 -noout -subject -issuer -dates -ext subjectAltName. Цепочку и причину отказа видно в выводе s_client. Быстрая проверка через curl: curl -vI https://host. Так я нахожу просроченный сертификат, неверное имя или неполную цепочку.

Что хотят услышать: openssl s_client с -servername, openssl x509, сроки и SAN, цепочка, curl -v.

Красный флаг: «Проверяю только в браузере».

13. [junior] Что такое Let’s Encrypt и ACME, и как работает автообновление сертификата?

Ответ

Let’s Encrypt - бесплатный центр сертификации, а ACME - протокол, по которому клиент (например, certbot) доказывает владение доменом и получает сертификат автоматически. Подтверждение бывает HTTP-01 (файл по HTTP на порту 80) или DNS-01 (TXT-запись, нужна для wildcard-сертификатов). Сертификаты короткоживущие (90 дней), поэтому обновление автоматическое: таймер или cron вызывает клиент, после чего nginx нужно перечитать (reload). Проверяю certbot renew --dry-run и слежу за сроком в мониторинге, чтобы узнать о сбое заранее.

Что хотят услышать: ACME-клиент, HTTP-01 и DNS-01, wildcard через DNS, короткий срок, reload после обновления, мониторинг срока.

Красный флаг: «Выпустил один раз и забыл».

Проверено на версиях

Практика прогонялась в контейнерах Docker на Mac. Основной стенд: образ devops-lab:24.04 (Ubuntu 24.04 с systemd): OpenSSL 3.0.13, nginx 1.24.0, curl 8.5.0, Python 3.12, tcpdump 4.99.4. Прогнаны задания 1-5 целиком (кроме коммита в git), скрипт поломок (сценарии 1, 2, 3 и fix, в том числе повторный fix), примеры с числами (Диффи-Хеллман и шифр Цезаря пересчитаны в Python, хеши посчитаны sha256sum).

Отдельно проверены различия на Ubuntu 26.04 (ubuntu:26.04, без systemd): OpenSSL 3.5.5, nginx 1.28.3, curl 8.18.0. Отличаются форматы вывода (CN=notes.lab без пробелов, тексты ошибок curl с номером (18), (10)), логика та же. Полный проект на 26.04 не прогонялся.

Не прогонялось: git commit в конце задания 5 (в стенде ~/notes не репозиторий git), поведение HSTS в браузере (нужен браузер, на стенде только curl), цепочка example.com (содержимое сайта меняется, в уроке показан вывод на день прогона, 30 сентября 2026), соответствие сроков публичных сертификатов решению CA/Browser Forum (сверяй на cabforum.org).

Итог урока: ты умеешь

  • объяснить, что даёт TLS (шифрование, целостность, проверка личности) и чем HTTPS отличается от HTTP;
  • различать симметричное и асимметричное шифрование, открытый и закрытый ключ, хеш и подпись;
  • пересказать рукопожатие TLS и объяснить, зачем нужен SNI;
  • прочитать сертификат (openssl x509 -text): Subject, Issuer, срок, SAN;
  • объяснить цепочку доверия и что должен отдавать сервер (fullchain);
  • выпустить самоподписанный сертификат и собственный мини-CA с промежуточным звеном;
  • включить HTTPS в nginx с редиректом и HSTS и проверить его curl и openssl s_client;
  • по тексту ошибки (self-signed, expired, no alternative subject name, unable to get local issuer) сказать, какая проверка не прошла;
  • проверить срок сертификата командой для мониторинга (-checkend).

Дальше: урок 2.7: файрвол.

Проверь себя

Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.

Тест работает с включённым JavaScript.

тема 2 урок 2.6 4 ч курс 0/0 ← → уроки