outposts: add docker TLS authentication and verification

This commit is contained in:
Jens Langhammer 2020-11-19 00:53:33 +01:00
parent 120f5f2e44
commit 0a8d4eecae
10 changed files with 189 additions and 16 deletions

View File

@ -43,5 +43,5 @@ COPY ./lifecycle/ /lifecycle
USER passbook
STOPSIGNAL SIGINT
ENV TMPDIR /dev/shm/
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]

View File

@ -33,7 +33,14 @@ class DockerServiceConnectionSerializer(ModelSerializer):
class Meta:
model = DockerServiceConnection
fields = ["pk", "name", "local", "url", "tls"]
fields = [
"pk",
"name",
"local",
"url",
"tls_verification",
"tls_authentication",
]
class DockerServiceConnectionViewSet(ModelViewSet):

View File

@ -70,5 +70,4 @@ class PassbookOutpostConfig(AppConfig):
name="Local Docker connection",
local=True,
url=unix_socket_path,
tls=True,
)

View File

@ -0,0 +1,56 @@
"""Create Docker TLSConfig from CertificateKeyPair"""
from pathlib import Path
from tempfile import gettempdir
from typing import Optional
from docker.tls import TLSConfig
from passbook.crypto.models import CertificateKeyPair
class DockerInlineTLS:
"""Create Docker TLSConfig from CertificateKeyPair"""
verification_kp: Optional[CertificateKeyPair]
authentication_kp: Optional[CertificateKeyPair]
def __init__(
self,
verification_kp: Optional[CertificateKeyPair],
authentication_kp: Optional[CertificateKeyPair],
) -> None:
self.verification_kp = verification_kp
self.authentication_kp = authentication_kp
def write_file(self, name: str, contents: str) -> str:
"""Wrapper for mkstemp that uses fdopen"""
path = Path(gettempdir(), name)
with open(path, "w") as _file:
_file.write(contents)
return str(path)
def write(self) -> TLSConfig:
"""Create TLSConfig with Certificate Keypairs"""
# So yes, this is quite ugly. But sadly, there is no clean way to pass
# docker-py (which is using requests (which is using urllib3)) a certificate
# for verification or authentication as string.
# Because we run in docker, and our tmpfs is isolated to us, we can just
# write out the certificates and keys to files and use their paths
config_args = {}
if self.verification_kp:
ca_cert_path = self.write_file(
f"{self.verification_kp.pk.hex}-cert.pem",
self.verification_kp.certificate_data,
)
config_args["ca_cert"] = ca_cert_path
if self.authentication_kp:
auth_cert_path = self.write_file(
f"{self.authentication_kp.pk.hex}-cert.pem",
self.authentication_kp.certificate_data,
)
auth_key_path = self.write_file(
f"{self.authentication_kp.pk.hex}-key.pem",
self.authentication_kp.key_data,
)
config_args["client_cert"] = (auth_cert_path, auth_key_path)
return TLSConfig(**config_args)

View File

@ -4,6 +4,7 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from passbook.admin.fields import CodeMirrorWidget, YAMLField
from passbook.crypto.models import CertificateKeyPair
from passbook.outposts.models import (
DockerServiceConnection,
KubernetesServiceConnection,
@ -46,17 +47,24 @@ class OutpostForm(forms.ModelForm):
class DockerServiceConnectionForm(forms.ModelForm):
"""Docker service-connection form"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["tls_authentication"].queryset = CertificateKeyPair.objects.filter(
key_data__isnull=False
)
class Meta:
model = DockerServiceConnection
fields = ["name", "local", "url", "tls"]
fields = ["name", "local", "url", "tls_verification", "tls_authentication"]
widgets = {
"name": forms.TextInput,
"url": forms.TextInput,
}
labels = {
"url": _("URL"),
"tls": _("TLS"),
"tls_verification": _("TLS Verification Certificate"),
"tls_authentication": _("TLS Authentication Certificate"),
}

View File

@ -20,10 +20,6 @@ def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaE
KubernetesServiceConnection = apps.get_model(
"passbook_outposts", "KubernetesServiceConnection"
)
from passbook.outposts.apps import PassbookOutpostConfig
# Ensure that local connection have been created
PassbookOutpostConfig.init_local_connection(None)
docker = DockerServiceConnection.objects.filter(local=True).first()
k8s = KubernetesServiceConnection.objects.filter(local=True).first()

View File

@ -0,0 +1,45 @@
# Generated by Django 3.1.3 on 2020-11-18 21:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_crypto", "0002_create_self_signed_kp"),
("passbook_outposts", "0010_service_connection"),
]
operations = [
migrations.RemoveField(
model_name="dockerserviceconnection",
name="tls",
),
migrations.AddField(
model_name="dockerserviceconnection",
name="tls_authentication",
field=models.ForeignKey(
blank=True,
default=None,
help_text="Certificate/Key used for authentication. Can be left empty for no authentication.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="+",
to="passbook_crypto.certificatekeypair",
),
),
migrations.AddField(
model_name="dockerserviceconnection",
name="tls_verification",
field=models.ForeignKey(
blank=True,
default=None,
help_text="CA which the endpoint's Certificate is verified against. Can be left empty for no validation.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="+",
to="passbook_crypto.certificatekeypair",
),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.1.3 on 2020-11-18 21:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_outposts", "0011_docker_tls_auth"),
]
operations = [
migrations.AlterField(
model_name="outpostserviceconnection",
name="local",
field=models.BooleanField(
default=False,
help_text="If enabled, use the local connection. Required Docker socket/Kubernetes Integration",
),
),
]

View File

@ -24,17 +24,21 @@ from kubernetes.config.incluster_config import load_incluster_config
from kubernetes.config.kube_config import load_kube_config_from_dict
from model_utils.managers import InheritanceManager
from packaging.version import LegacyVersion, Version, parse
from structlog import get_logger
from urllib3.exceptions import HTTPError
from passbook import __version__
from passbook.core.models import Provider, Token, TokenIntents, User
from passbook.crypto.models import CertificateKeyPair
from passbook.lib.config import CONFIG
from passbook.lib.models import InheritanceForeignKey
from passbook.lib.sentry import SentryIgnoredException
from passbook.lib.utils.template import render_to_string
from passbook.outposts.docker_tls import DockerInlineTLS
OUR_VERSION = parse(__version__)
OUTPOST_HELLO_INTERVAL = 10
LOGGER = get_logger()
class ServiceConnectionInvalid(SentryIgnoredException):
@ -99,7 +103,6 @@ class OutpostServiceConnection(models.Model):
local = models.BooleanField(
default=False,
unique=True,
help_text=_(
(
"If enabled, use the local connection. Required Docker "
@ -138,7 +141,31 @@ class DockerServiceConnection(OutpostServiceConnection):
"""Service Connection to a Docker endpoint"""
url = models.TextField()
tls = models.BooleanField()
tls_verification = models.ForeignKey(
CertificateKeyPair,
null=True,
blank=True,
default=None,
related_name="+",
on_delete=models.SET_DEFAULT,
help_text=_(
(
"CA which the endpoint's Certificate is verified against. "
"Can be left empty for no validation."
)
),
)
tls_authentication = models.ForeignKey(
CertificateKeyPair,
null=True,
blank=True,
default=None,
related_name="+",
on_delete=models.SET_DEFAULT,
help_text=_(
"Certificate/Key used for authentication. Can be left empty for no authentication."
),
)
@property
def form(self) -> Type[ModelForm]:
@ -158,10 +185,14 @@ class DockerServiceConnection(OutpostServiceConnection):
else:
client = DockerClient(
base_url=self.url,
tls=self.tls,
tls=DockerInlineTLS(
verification_kp=self.tls_verification,
authentication_kp=self.tls_authentication,
).write(),
)
client.containers.list()
except DockerException as exc:
LOGGER.error(exc)
raise ServiceConnectionInvalid from exc
return client

View File

@ -6860,7 +6860,6 @@ definitions:
required:
- name
- url
- tls
type: object
properties:
pk:
@ -6881,9 +6880,20 @@ definitions:
title: Url
type: string
minLength: 1
tls:
title: Tls
type: boolean
tls_verification:
title: Tls verification
description: CA which the endpoint's Certificate is verified against. Can
be left empty for no validation.
type: string
format: uuid
x-nullable: true
tls_authentication:
title: Tls authentication
description: Certificate/Key used for authentication. Can be left empty for
no authentication.
type: string
format: uuid
x-nullable: true
KubernetesServiceConnection:
description: KubernetesServiceConnection Serializer
required: