outposts: add docker TLS authentication and verification
This commit is contained in:
		@ -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,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										56
									
								
								passbook/outposts/docker_tls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								passbook/outposts/docker_tls.py
									
									
									
									
									
										Normal 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)
 | 
			
		||||
@ -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()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										45
									
								
								passbook/outposts/migrations/0011_docker_tls_auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								passbook/outposts/migrations/0011_docker_tls_auth.py
									
									
									
									
									
										Normal 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",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -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
	
	Block a user