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 USER passbook
STOPSIGNAL SIGINT STOPSIGNAL SIGINT
ENV TMPDIR /dev/shm/
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ] ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]

View File

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

View File

@ -70,5 +70,4 @@ class PassbookOutpostConfig(AppConfig):
name="Local Docker connection", name="Local Docker connection",
local=True, local=True,
url=unix_socket_path, 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 django.utils.translation import gettext_lazy as _
from passbook.admin.fields import CodeMirrorWidget, YAMLField from passbook.admin.fields import CodeMirrorWidget, YAMLField
from passbook.crypto.models import CertificateKeyPair
from passbook.outposts.models import ( from passbook.outposts.models import (
DockerServiceConnection, DockerServiceConnection,
KubernetesServiceConnection, KubernetesServiceConnection,
@ -46,17 +47,24 @@ class OutpostForm(forms.ModelForm):
class DockerServiceConnectionForm(forms.ModelForm): class DockerServiceConnectionForm(forms.ModelForm):
"""Docker service-connection form""" """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: class Meta:
model = DockerServiceConnection model = DockerServiceConnection
fields = ["name", "local", "url", "tls"] fields = ["name", "local", "url", "tls_verification", "tls_authentication"]
widgets = { widgets = {
"name": forms.TextInput, "name": forms.TextInput,
"url": forms.TextInput, "url": forms.TextInput,
} }
labels = { labels = {
"url": _("URL"), "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( KubernetesServiceConnection = apps.get_model(
"passbook_outposts", "KubernetesServiceConnection" "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() docker = DockerServiceConnection.objects.filter(local=True).first()
k8s = KubernetesServiceConnection.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 kubernetes.config.kube_config import load_kube_config_from_dict
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from packaging.version import LegacyVersion, Version, parse from packaging.version import LegacyVersion, Version, parse
from structlog import get_logger
from urllib3.exceptions import HTTPError from urllib3.exceptions import HTTPError
from passbook import __version__ from passbook import __version__
from passbook.core.models import Provider, Token, TokenIntents, User from passbook.core.models import Provider, Token, TokenIntents, User
from passbook.crypto.models import CertificateKeyPair
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
from passbook.lib.models import InheritanceForeignKey from passbook.lib.models import InheritanceForeignKey
from passbook.lib.sentry import SentryIgnoredException from passbook.lib.sentry import SentryIgnoredException
from passbook.lib.utils.template import render_to_string from passbook.lib.utils.template import render_to_string
from passbook.outposts.docker_tls import DockerInlineTLS
OUR_VERSION = parse(__version__) OUR_VERSION = parse(__version__)
OUTPOST_HELLO_INTERVAL = 10 OUTPOST_HELLO_INTERVAL = 10
LOGGER = get_logger()
class ServiceConnectionInvalid(SentryIgnoredException): class ServiceConnectionInvalid(SentryIgnoredException):
@ -99,7 +103,6 @@ class OutpostServiceConnection(models.Model):
local = models.BooleanField( local = models.BooleanField(
default=False, default=False,
unique=True,
help_text=_( help_text=_(
( (
"If enabled, use the local connection. Required Docker " "If enabled, use the local connection. Required Docker "
@ -138,7 +141,31 @@ class DockerServiceConnection(OutpostServiceConnection):
"""Service Connection to a Docker endpoint""" """Service Connection to a Docker endpoint"""
url = models.TextField() 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 @property
def form(self) -> Type[ModelForm]: def form(self) -> Type[ModelForm]:
@ -158,10 +185,14 @@ class DockerServiceConnection(OutpostServiceConnection):
else: else:
client = DockerClient( client = DockerClient(
base_url=self.url, base_url=self.url,
tls=self.tls, tls=DockerInlineTLS(
verification_kp=self.tls_verification,
authentication_kp=self.tls_authentication,
).write(),
) )
client.containers.list() client.containers.list()
except DockerException as exc: except DockerException as exc:
LOGGER.error(exc)
raise ServiceConnectionInvalid from exc raise ServiceConnectionInvalid from exc
return client return client

View File

@ -6860,7 +6860,6 @@ definitions:
required: required:
- name - name
- url - url
- tls
type: object type: object
properties: properties:
pk: pk:
@ -6881,9 +6880,20 @@ definitions:
title: Url title: Url
type: string type: string
minLength: 1 minLength: 1
tls: tls_verification:
title: Tls title: Tls verification
type: boolean 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: KubernetesServiceConnection:
description: KubernetesServiceConnection Serializer description: KubernetesServiceConnection Serializer
required: required: