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
|
USER passbook
|
||||||
STOPSIGNAL SIGINT
|
STOPSIGNAL SIGINT
|
||||||
|
ENV TMPDIR /dev/shm/
|
||||||
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]
|
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 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"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 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
|
||||||
|
|
||||||
|
|
18
swagger.yaml
18
swagger.yaml
|
@ -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:
|
||||||
|
|
Reference in New Issue