Post

Работа с Nexus по HTTPS в условиях просроченного SSL-сертификата

Работа с Nexus по HTTPS в условиях просроченного SSL-сертификата

В ряде проектов Nexus может использоваться не как Maven-репозиторий, а как обычное хранилище файлов. Архивы публикуются из backend-сервисов и затем используются по прямым URL — без dependency resolution и без участия build-инструментов (Maven, Gradle и тп) на стороне потребителя.

Рассмотрим ситуацию, в которой такая публикация выполняется по HTTPS, но SSL-сертификат репозитория оказывается просрочен. При этом Nexus продолжает открываться в браузере, а файлы доступны для ручного скачивания, однако любые попытки автоматизированной загрузки из Java-приложения завершаются ошибкой.

Обновление сертификата в данном случае находится вне зоны ответственности сервиса. Задача статьи — не обсуждать корректность инфраструктурных решений, а показать, как технически организовать загрузку артефактов в Nexus в таких условиях, какие ограничения при этом возникают и на что важно обратить внимание при реализации подобного подхода.

Сценарий использования Nexus

Такой подход часто встречается для служебных артефактов, патчей или промежуточных сборок, которые не являются зависимостями в привычном смысле. Nexus в этом случае выступает удобным и централизованным хранилищем с контролем доступа, а не частью системы управления зависимостями.

Важно зафиксировать, что в данном контексте не используется mvn deploy и не формируются Maven-метаданные (pom.xml, checksums и т. п.). Речь идет о прямой загрузке файлов в репозиторий и последующем доступе к ним по HTTP, что накладывает свои требования на реализацию клиента, но существенно упрощает модель использования.

Как Java-приложение взаимодействует с Nexus по HTTPS

При обращении к Nexus по HTTPS Java-приложение использует стандартные механизмы TLS, встроенные в JVM. В простейшем случае для этого достаточно стандартного клиента:

1
HttpClient client = HttpClient.newHttpClient();

Независимо от используемой библиотеки или уровня абстракции, каждое HTTPS-соединение начинается с TLS-рукопожатия. На этом этапе JVM проверяет SSL-сертификат сервера и цепочку доверия, по которой этот сертификат был выдан.

Важно понимать, что проверка сертификата выполняется до отправки HTTP-запроса. Если сертификат сервера недействителен, соединение не устанавливается, и код приложения до логики загрузки файла или обработки ответа просто не доходит.

В рассматриваемом сценарии ошибка возникает именно на этом этапе — в момент проверки сертификата. Формат запроса, HTTP-метод, заголовки и учетные данные не имеют значения: при некорректном SSL-сертификате сервер считается недоверенным, и HTTPS-соединение разрывается еще до выполнения запроса.

Минимальный контекст про SSL в Java

При установлении HTTPS-соединения JVM выполняет набор обязательных проверок, от которых зависит, будет ли соединение установлено. Эти проверки не зависят от прикладного кода и выполняются на уровне TLS.

В базовом виде проверяются следующие условия:

  • Срок действия сертификата сервера;
  • Корректность цепочки доверия;
  • Наличие доверенного корневого или промежуточного сертификата в truststore.

Если хотя бы одно из этих условий не выполняется, соединение считается небезопасным и разрывается.

Отдельно важно отметить отсутствие интерактивных исключений. В отличие от браузера, Java-приложение не может запросить подтверждение у пользователя или временно игнорировать проблему сертификата. Любая ошибка SSL в автоматическом клиенте приводит к исключению и завершению операции.

Как именно Java устанавливает HTTPS-соединение

Важно понимать, в какой момент и на каком уровне возникает ошибка при работе с HTTPS. Часто кажется, что проблема появляется при отправке запроса, но на самом деле HTTP на этом этапе еще даже не начинается.

Для HTTPS-соединения последовательность шагов выглядит так.

1. Установка TCP-соединения

На первом шаге клиент открывает обычное TCP-соединение:

  • Создается TCP-сокет к host:443;
  • Это чистый сетевой коннект, без TLS и без HTTP;
  • Никакие данные прикладного уровня еще не передаются.

Если на этом этапе есть проблема (порт закрыт, сервис недоступен, firewall), Java выбросит типичную ошибку уровня сети, например ConnectException. Этот шаг успешно проходит даже при просроченном сертификате.

2. TLS / SSL-рукопожатие (Handshake)

После установления TCP-соединения начинается TLS-рукопожатие. Это отдельный протокол, который выполняется до HTTP.

На этом этапе происходит следующее:

  • клиент отправляет служебное сообщение «Я хочу говорить по TLS, вот версии и криптографические алгоритмы, которые поддерживаю»;
  • сервер отвечает: присылает свой SSL-сертификат, при необходимости — цепочку сертификатов (серверный + промежуточные).
  • клиент выполняет проверки: срок действия сертификата, цепочку доверия до корневого CA, соответствие сертификата доменному имени.

Именно на этом этапе Java обнаруживает проблему и выбрасывает CertificateExpiredException — TLS-рукопожатие прерывается, соединение считается небезопасным.

3. Только после успешного TLS — HTTP

Только если TLS-рукопожатие завершилось успешно — считается, что защищенный канал установлен. В этот канал отправляется HTTP-запрос:

  • Метод (PUT),
  • URL,
  • Заголовки,
  • Тело запроса (файл).

Если TLS не прошел — HTTP не существует вообще, до этого уровня выполнение просто не доходит.

Почему ошибка возникает до отправки запроса

Хотя сетевое взаимодействие уже происходит, важно понимать его характер. TLS-обмен служебными сообщениями:

  • Не имеет URL;
  • Не имеет HTTP-метода;
  • Не виден серверному приложению (в данном случае — Nexus).

Эти сообщения обрабатываются не кодом приложения и не веб-сервером, а TLS-стеком:

  • в Java — это JSSE (Java Secure Socket Extension);
  • в других платформах — аналогичные системные реализации TLS.

С точки зрения Nexus никакого HTTP-запроса не было вовсе. С точки зрения Java-приложения соединение было прервано еще на этапе проверки безопасности.

Подход к загрузке артефакта в таких условиях

В рассматриваемом сценарии задача формулируется предельно прагматично: не исправить инфраструктуру, а обеспечить загрузку артефакта в условиях, где SSL-сертификат репозитория просрочен и не может быть обновлен в рамках текущей ответственности.

Ключевая идея решения — локализовать небезопасную конфигурацию и не протекать ею в остальной код приложения.

Приняты следующие принципы:

  • Используется отдельный HTTP-клиент, предназначенный только для загрузки в Nexus;
  • Стандартный HttpClient приложения не переопределяется;
  • Отключение SSL-проверки выполняется точечно, а не глобально;
  • Небезопасная конфигурация не влияет на другие HTTP-вызовы;
  • Решение легко удалить после исправления сертификата.

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

Отключение SSL-проверки локально

Для работы с просроченным сертификатом создается отдельный HttpClient с кастомным SSLContext, который доверяет всем сертификатам:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private HttpClient insecureHttpClient() {
    try {
        TrustManager[] trustAll = new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] chain, String authType) {
                    }

                    @Override
                    public void checkServerTrusted(X509Certificate[] chain, String authType) {
                    }

                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                    }
                }
        };

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustAll, new SecureRandom());

        return HttpClient.newBuilder()
                .sslContext(sslContext)
                .build();

    } catch (Exception e) {
        throw new IllegalStateException("Failed to create insecure HttpClient", e);
    }
}

Загрузка файла

1
2
3
4
5
6
HttpRequest request = HttpRequest.newBuilder(uri)
        .PUT(HttpRequest.BodyPublishers.ofFile(zipFile))
        .header("Content-Type", "application/zip")
        .header("Authorization", basicAuth(username, password))
        .build();

Используется базовая HTTP-аутентификация:

1
2
3
4
5
private String basicAuth(String user, String pass) {
    String raw = user + ":" + pass;
    return "Basic " + Base64.getEncoder().encodeToString(raw.getBytes());
}

И запрос отправляется через ранее созданный небезопасный клиент:

1
2
3
4
5
6
7
8
9
10
HttpClient client = insecureHttpClient();
HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());

if (response.statusCode() >= 300) {
    throw new IllegalStateException(
            "Nexus upload failed: " + response.statusCode() + " " + response.body()
    );
}

Почему это решение осознанное

Такой подход:

  • не требует модификации JVM truststore;
  • не отключает SSL глобально;
  • не влияет на другие сервисы;
  • прозрачен и легко контролируем;
  • временный по своей природе.

Когда сертификат в Nexus будет обновлен, этот код можно удалить, не затрагивая остальную архитектуру.

This post is licensed under CC BY 4.0 by the author.