outposts: add docker TLS authentication and verification
This commit is contained in:
parent
120f5f2e44
commit
0a8d4eecae
|
@ -43,5 +43,5 @@ COPY ./lifecycle/ /lifecycle
|
|||
|
||||
USER passbook
|
||||
STOPSIGNAL SIGINT
|
||||
|
||||
ENV TMPDIR /dev/shm/
|
||||
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -70,5 +70,4 @@ class PassbookOutpostConfig(AppConfig):
|
|||
name="Local Docker connection",
|
||||
local=True,
|
||||
url=unix_socket_path,
|
||||
tls=True,
|
||||
)
|
||||
|
|
|
@ -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)
|
|
@ -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"),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
||||
|
|
18
swagger.yaml
18
swagger.yaml
|
@ -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:
|
||||
|
|
Reference in New Issue