Docker Remote API с аутентификацией по сертификату с проверкой отзыва
Для нужд удаленного управления Docker’ом, Docker умеет предоставлять веб-API.
Это API может как вовсе не требовать аутентификации (что крайне не рекомендуется), так и использовать аутентификация по сертификату.
Проблема заключается в том, что родная аутентификация по сертификату не предусматривает проверку отзыва сертификата. И это может иметь серьезные последствия.
Я хочу рассказать как я решил эту проблему.
Для начала следует сказать что говорить я буду про Docker для Windows. Возможно в Linux все не так плохо, но сейчас не об этом.
Что мы имеем? У нас есть Docker, с вот таким конфигом:
{
"hosts": ["tcp://0.0.0.0:2376", "npipe://"],
"tlsverify": true,
"tlscacert": "C:\\ssl\\ca.cer",
"tlscert": "C:\\ssl\\server.cer",
"tlskey": "C:\\ssl\\server.key"
}
Клиенты могут подключаться со своими сертификатами, но эти сертификаты не проверяются на предмет отзыва.
Идея решения проблемы заключается в том, чтобы написать свой прокси-сервис, который выступал бы в качестве посредника. Наш сервис будет установлен на том же сервере что и Docker, заберет себе порт 2376, будет общаться с Docker по //./pipe/docker_engine.
Недолго думая я создал ASP.NET Core проект и сделал простейшее проксирование:
app.Run(async (context) =>
{
var certificate = context.Connection.ClientCertificate;
if (certificate != null)
{
logger.LogInformation($"Certificate subject: {certificate.Subject}, serial: {certificate.SerialNumber}");
}
var handler = new ManagedHandler(async (host, port, cancellationToken) =>
{
var stream = new NamedPipeClientStream(".", "docker_engine", PipeDirection.InOut, PipeOptions.Asynchronous);
var dockerStream = new DockerPipeStream(stream);
await stream.ConnectAsync(NamedPipeConnectTimeout.Milliseconds, cancellationToken);
return dockerStream;
});
using (var client = new HttpClient(handler, true))
{
var method = new HttpMethod(context.Request.Method);
var builder = new UriBuilder("http://dockerengine")
{
Path = context.Request.Path,
Query = context.Request.QueryString.ToUriComponent()
};
using (var request = new HttpRequestMessage(method, builder.Uri))
{
request.Version = new Version(1, 11);
request.Headers.Add("User-Agent", "proxy");
if (method != HttpMethod.Get)
{
request.Content = new StreamContent(context.Request.Body);
request.Content.Headers.ContentType = new MediaTypeHeaderValue(context.Request.ContentType);
}
using (var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted))
{
context.Response.ContentType = response.Content.Headers.ContentType.ToString();
var output = await response.Content.ReadAsStreamAsync();
await output.CopyToAsync(context.Response.Body, 4096, context.RequestAborted);
}
}
}
});
Этого оказалось достаточно для простых запросов GET и POST из Docker API. Но этого мало, т.к. для более сложных операций (требующий пользовательской ввод) Docker использует что-то похожее на WebSocket. Засада была в том, что Kestrel наотрез отказывался принимать запросы, которые приходили от Docker Client, мотивируя это тем, что в запросе с заголовком Connection: Upgrade не может быть тела. А оно было.
Пришлось отказаться от Kestrel и написать чуть больше кода. По сути — свой web сервер. Самостоятельно открывать порт, создавать TLS соединение, парсить HTTP заголовки, устанавливать внутреннее соединение с Docker и обмениваться потоками ввода-вывода. И это сработало.
Исходники можно посмотреть здесь.
Итак, приложение написано и надо бы его как-то запускать. Идея заключается в том, чтобы создать контейнер с нашим приложением, прокинуть внутрь npine:// и опубликовать порт 2376
Для сборки образа нам потребуется публичный сертификат центра сертификации (ca.cer), который будет выдавать сертификаты пользователям.
Этот сертификат будет установлен в доверенные корневые центры сертификации контейнера, в котором будет запущен наш прокси.
Установка его необходима для процедуры проверки сертификата.
Я не заморачивался написанием такого Docker-файла, который сам бы собирал приложение.
Поэтому его надо собрать самостоятельно. Из папки с dockerfile запускаем:
dotnet publish -c Release -o ..\publish .\DockerTLS\DockerTLS.csproj
Сейчас у нас должны быть: Dockerfile
, publish
, ca.cer
. Собираем образ:
docker build -t vitaliyorg.azurecr.io/docker/proxy:1809 .
docker push vitaliyorg.azurecr.io/docker/proxy:1809
Разумеется, имя образа может быть любое.
Для запуска контейнера нам понадобятся сертификат сервера certificate.pfx
и файл с паролем password.txt
. Все содержимое файла считается паролем. Поэтому лишних переводов строк быть не должно.
Пусть все это добро находится в папке: c:\data
на сервере, где установлен Docker.
На этом же сервере запускаем:
docker run --name docker-proxy -d -v "c:/data:c:/data" -v \\.\pipe\docker_engine:\\.\pipe\docker_engine --restart always -p 2376:2376 vitaliyorg.azurecr.io/docker/proxy:1809
С помощью docker logs
можно видеть кто что делал. Там же можно видеть попытки подключения, которые завершились неудачно.