outposts: add remote docker integration via SSH

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-12-25 16:31:34 +01:00
parent 19b707a0fb
commit 6510b97c1e
14 changed files with 397 additions and 103 deletions

View File

@ -9,7 +9,11 @@ from structlog.testing import capture_logs
from authentik import ENV_GIT_HASH_KEY, __version__ from authentik import ENV_GIT_HASH_KEY, __version__
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.outposts.models import Outpost, OutpostServiceConnection from authentik.outposts.models import (
Outpost,
OutpostServiceConnection,
OutpostServiceConnectionState,
)
FIELD_MANAGER = "goauthentik.io" FIELD_MANAGER = "goauthentik.io"
@ -28,11 +32,25 @@ class DeploymentPort:
inner_port: Optional[int] = None inner_port: Optional[int] = None
class BaseClient:
"""Base class for custom clients"""
def fetch_state(self) -> OutpostServiceConnectionState:
"""Get state, version info"""
raise NotImplementedError
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
"""Cleanup after usage"""
class BaseController: class BaseController:
"""Base Outpost deployment controller""" """Base Outpost deployment controller"""
deployment_ports: list[DeploymentPort] deployment_ports: list[DeploymentPort]
client: BaseClient
outpost: Outpost outpost: Outpost
connection: OutpostServiceConnection connection: OutpostServiceConnection
@ -63,6 +81,13 @@ class BaseController:
self.down() self.down()
return [x["event"] for x in logs] return [x["event"] for x in logs]
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
"""Cleanup after usage"""
self.client.__exit__()
def get_static_deployment(self) -> str: def get_static_deployment(self) -> str:
"""Return a static deployment configuration""" """Return a static deployment configuration"""
raise NotImplementedError raise NotImplementedError

View File

@ -1,17 +1,75 @@
"""Docker controller""" """Docker controller"""
from time import sleep from time import sleep
from typing import Optional
from urllib.parse import urlparse
from django.conf import settings from django.conf import settings
from django.utils.text import slugify from django.utils.text import slugify
from docker import DockerClient from docker import DockerClient as UpstreamDockerClient
from docker.errors import DockerException, NotFound from docker.errors import DockerException, NotFound
from docker.models.containers import Container from docker.models.containers import Container
from docker.utils.utils import kwargs_from_env
from structlog.stdlib import get_logger
from yaml import safe_dump from yaml import safe_dump
from authentik import __version__ from authentik import __version__
from authentik.outposts.controllers.base import BaseController, ControllerException from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
from authentik.outposts.docker_ssh import DockerInlineSSH
from authentik.outposts.docker_tls import DockerInlineTLS
from authentik.outposts.managed import MANAGED_OUTPOST from authentik.outposts.managed import MANAGED_OUTPOST
from authentik.outposts.models import DockerServiceConnection, Outpost, ServiceConnectionInvalid from authentik.outposts.models import (
DockerServiceConnection,
Outpost,
OutpostServiceConnectionState,
ServiceConnectionInvalid,
)
class DockerClient(UpstreamDockerClient, BaseClient):
"""Custom docker client, which can handle TLS and SSH from a database."""
tls: Optional[DockerInlineTLS]
ssh: Optional[DockerInlineSSH]
def __init__(self, connection: DockerServiceConnection):
self.tls = None
self.ssh = None
if connection.local:
# Same result as DockerClient.from_env
super().__init__(kwargs_from_env())
else:
parsed_url = urlparse(connection.url)
tls_config = False
if parsed_url.scheme == "ssh":
self.ssh = DockerInlineSSH(parsed_url.hostname, connection.tls_authentication)
self.ssh.write()
else:
self.tls = DockerInlineTLS(
verification_kp=connection.tls_verification,
authentication_kp=connection.tls_authentication,
)
tls_config = self.tls.write()
super().__init__(
base_url=connection.url,
tls=tls_config,
)
self.logger = get_logger()
# Ensure the client actually works
self.containers.list()
def fetch_state(self) -> OutpostServiceConnectionState:
try:
return OutpostServiceConnectionState(version=self.info()["ServerVersion"], healthy=True)
except (ServiceConnectionInvalid, DockerException):
return OutpostServiceConnectionState(version="", healthy=False)
def __exit__(self, exc_type, exc_value, traceback):
if self.tls:
self.logger.debug("Cleaning up TLS")
self.tls.cleanup()
if self.ssh:
self.logger.debug("Cleaning up SSH")
self.ssh.cleanup()
class DockerController(BaseController): class DockerController(BaseController):
@ -27,8 +85,9 @@ class DockerController(BaseController):
if outpost.managed == MANAGED_OUTPOST: if outpost.managed == MANAGED_OUTPOST:
return return
try: try:
self.client = connection.client() self.client = DockerClient(connection)
except ServiceConnectionInvalid as exc: except DockerException as exc:
self.logger.warning(exc)
raise ControllerException from exc raise ControllerException from exc
@property @property

View File

@ -2,19 +2,53 @@
from io import StringIO from io import StringIO
from typing import Type from typing import Type
from kubernetes.client import VersionApi, VersionInfo
from kubernetes.client.api_client import ApiClient from kubernetes.client.api_client import ApiClient
from kubernetes.client.configuration import Configuration
from kubernetes.client.exceptions import OpenApiException from kubernetes.client.exceptions import OpenApiException
from kubernetes.config.config_exception import ConfigException
from kubernetes.config.incluster_config import load_incluster_config
from kubernetes.config.kube_config import load_kube_config_from_dict
from structlog.testing import capture_logs from structlog.testing import capture_logs
from urllib3.exceptions import HTTPError from urllib3.exceptions import HTTPError
from yaml import dump_all from yaml import dump_all
from authentik.outposts.controllers.base import BaseController, ControllerException from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
from authentik.outposts.controllers.k8s.secret import SecretReconciler from authentik.outposts.controllers.k8s.secret import SecretReconciler
from authentik.outposts.controllers.k8s.service import ServiceReconciler from authentik.outposts.controllers.k8s.service import ServiceReconciler
from authentik.outposts.controllers.k8s.service_monitor import PrometheusServiceMonitorReconciler from authentik.outposts.controllers.k8s.service_monitor import PrometheusServiceMonitorReconciler
from authentik.outposts.models import KubernetesServiceConnection, Outpost, ServiceConnectionInvalid from authentik.outposts.models import (
KubernetesServiceConnection,
Outpost,
OutpostServiceConnectionState,
ServiceConnectionInvalid,
)
class KubernetesClient(ApiClient, BaseClient):
"""Custom kubernetes client based on service connection"""
def __init__(self, connection: KubernetesServiceConnection):
config = Configuration()
try:
if connection.local:
load_incluster_config(client_configuration=config)
else:
load_kube_config_from_dict(connection.kubeconfig, client_configuration=config)
super().__init__(config)
except ConfigException as exc:
raise ServiceConnectionInvalid from exc
def fetch_state(self) -> OutpostServiceConnectionState:
"""Get version info"""
try:
api_instance = VersionApi(self)
version: VersionInfo = api_instance.get_code()
return OutpostServiceConnectionState(version=version.git_version, healthy=True)
except (OpenApiException, HTTPError, ServiceConnectionInvalid):
return OutpostServiceConnectionState(version="", healthy=False)
class KubernetesController(BaseController): class KubernetesController(BaseController):
@ -23,12 +57,12 @@ class KubernetesController(BaseController):
reconcilers: dict[str, Type[KubernetesObjectReconciler]] reconcilers: dict[str, Type[KubernetesObjectReconciler]]
reconcile_order: list[str] reconcile_order: list[str]
client: ApiClient client: KubernetesClient
connection: KubernetesServiceConnection connection: KubernetesServiceConnection
def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection) -> None: def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection) -> None:
super().__init__(outpost, connection) super().__init__(outpost, connection)
self.client = connection.client() self.client = KubernetesClient(connection)
self.reconcilers = { self.reconcilers = {
"secret": SecretReconciler, "secret": SecretReconciler,
"deployment": DeploymentReconciler, "deployment": DeploymentReconciler,

View File

@ -0,0 +1,77 @@
"""Docker SSH helper"""
import os
from pathlib import Path
from tempfile import gettempdir
from authentik.crypto.models import CertificateKeyPair
HEADER = "### Managed by authentik"
FOOTER = "### End Managed by authentik"
def opener(path, flags):
"""File opener to create files as 700 perms"""
return os.open(path, flags, 0o700)
class DockerInlineSSH:
"""Create paramiko ssh config from CertificateKeyPair"""
host: str
keypair: CertificateKeyPair
key_path: str
config_path: Path
header: str
def __init__(self, host: str, keypair: CertificateKeyPair) -> None:
self.host = host
self.keypair = keypair
self.config_path = Path("~/.ssh/config").expanduser()
self.header = f"{HEADER} - {self.host}\n"
def write_config(self, key_path: str) -> bool:
"""Update the local user's ssh config file"""
with open(self.config_path, "a+", encoding="utf-8") as ssh_config:
if self.header in ssh_config.readlines():
return False
ssh_config.writelines(
[
self.header,
f"Host {self.host}\n",
f" IdentityFile {key_path}\n",
f"{FOOTER}\n",
"\n",
]
)
return True
def write_key(self):
"""Write keypair's private key to a temporary file"""
path = Path(gettempdir(), f"{self.keypair.pk}_private.pem")
with open(path, "w", encoding="utf8", opener=opener) as _file:
_file.write(self.keypair.key_data)
return str(path)
def write(self):
"""Write keyfile and update ssh config"""
self.key_path = self.write_key()
was_written = self.write_config(self.key_path)
if not was_written:
self.cleanup()
def cleanup(self):
"""Cleanup when we're done"""
os.unlink(self.key_path)
with open(self.config_path, "r+", encoding="utf-8") as ssh_config:
start = 0
end = 0
lines = ssh_config.readlines()
for idx, line in enumerate(lines):
if line == self.header:
start = idx
if start != 0 and line == f"{FOOTER}\n":
end = idx
with open(self.config_path, "w+", encoding="utf-8") as ssh_config:
lines = lines[:start] + lines[end + 2 :]
ssh_config.writelines(lines)

View File

@ -1,4 +1,5 @@
"""Create Docker TLSConfig from CertificateKeyPair""" """Create Docker TLSConfig from CertificateKeyPair"""
from os import unlink
from pathlib import Path from pathlib import Path
from tempfile import gettempdir from tempfile import gettempdir
from typing import Optional from typing import Optional
@ -14,6 +15,8 @@ class DockerInlineTLS:
verification_kp: Optional[CertificateKeyPair] verification_kp: Optional[CertificateKeyPair]
authentication_kp: Optional[CertificateKeyPair] authentication_kp: Optional[CertificateKeyPair]
_paths: list[str]
def __init__( def __init__(
self, self,
verification_kp: Optional[CertificateKeyPair], verification_kp: Optional[CertificateKeyPair],
@ -21,14 +24,21 @@ class DockerInlineTLS:
) -> None: ) -> None:
self.verification_kp = verification_kp self.verification_kp = verification_kp
self.authentication_kp = authentication_kp self.authentication_kp = authentication_kp
self._paths = []
def write_file(self, name: str, contents: str) -> str: def write_file(self, name: str, contents: str) -> str:
"""Wrapper for mkstemp that uses fdopen""" """Wrapper for mkstemp that uses fdopen"""
path = Path(gettempdir(), name) path = Path(gettempdir(), name)
with open(path, "w", encoding="utf8") as _file: with open(path, "w", encoding="utf8") as _file:
_file.write(contents) _file.write(contents)
self._paths.append(str(path))
return str(path) return str(path)
def cleanup(self):
"""Clean up certificates when we're done"""
for path in self._paths:
unlink(path)
def write(self) -> TLSConfig: def write(self) -> TLSConfig:
"""Create TLSConfig with Certificate Key pairs""" """Create TLSConfig with Certificate Key pairs"""
# So yes, this is quite ugly. But sadly, there is no clean way to pass # So yes, this is quite ugly. But sadly, there is no clean way to pass

View File

@ -11,21 +11,11 @@ from django.core.cache import cache
from django.db import IntegrityError, models, transaction from django.db import IntegrityError, models, transaction
from django.db.models.base import Model from django.db.models.base import Model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from docker.client import DockerClient
from docker.errors import DockerException
from guardian.models import UserObjectPermission from guardian.models import UserObjectPermission
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
from kubernetes.client import VersionApi, VersionInfo
from kubernetes.client.api_client import ApiClient
from kubernetes.client.configuration import Configuration
from kubernetes.client.exceptions import OpenApiException
from kubernetes.config.config_exception import ConfigException
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 model_utils.managers import InheritanceManager
from packaging.version import LegacyVersion, Version, parse from packaging.version import LegacyVersion, Version, parse
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from urllib3.exceptions import HTTPError
from authentik import ENV_GIT_HASH_KEY, __version__ from authentik import ENV_GIT_HASH_KEY, __version__
from authentik.core.models import ( from authentik.core.models import (
@ -44,7 +34,6 @@ from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.errors import exception_to_string from authentik.lib.utils.errors import exception_to_string
from authentik.managed.models import ManagedModel from authentik.managed.models import ManagedModel
from authentik.outposts.controllers.k8s.utils import get_namespace from authentik.outposts.controllers.k8s.utils import get_namespace
from authentik.outposts.docker_tls import DockerInlineTLS
from authentik.tenants.models import Tenant from authentik.tenants.models import Tenant
OUR_VERSION = parse(__version__) OUR_VERSION = parse(__version__)
@ -150,10 +139,6 @@ class OutpostServiceConnection(models.Model):
return OutpostServiceConnectionState("", False) return OutpostServiceConnectionState("", False)
return state return state
def fetch_state(self) -> OutpostServiceConnectionState:
"""Fetch current Service Connection state"""
raise NotImplementedError
@property @property
def component(self) -> str: def component(self) -> str:
"""Return component used to edit this object""" """Return component used to edit this object"""
@ -211,35 +196,6 @@ class DockerServiceConnection(OutpostServiceConnection):
def __str__(self) -> str: def __str__(self) -> str:
return f"Docker Service-Connection {self.name}" return f"Docker Service-Connection {self.name}"
def client(self) -> DockerClient:
"""Get DockerClient"""
try:
client = None
if self.local:
client = DockerClient.from_env()
else:
client = DockerClient(
base_url=self.url,
tls=DockerInlineTLS(
verification_kp=self.tls_verification,
authentication_kp=self.tls_authentication,
).write(),
)
client.containers.list()
except DockerException as exc:
LOGGER.warning(exc)
raise ServiceConnectionInvalid from exc
return client
def fetch_state(self) -> OutpostServiceConnectionState:
try:
client = self.client()
return OutpostServiceConnectionState(
version=client.info()["ServerVersion"], healthy=True
)
except ServiceConnectionInvalid:
return OutpostServiceConnectionState(version="", healthy=False)
class Meta: class Meta:
verbose_name = _("Docker Service-Connection") verbose_name = _("Docker Service-Connection")
@ -266,27 +222,6 @@ class KubernetesServiceConnection(OutpostServiceConnection):
def __str__(self) -> str: def __str__(self) -> str:
return f"Kubernetes Service-Connection {self.name}" return f"Kubernetes Service-Connection {self.name}"
def fetch_state(self) -> OutpostServiceConnectionState:
try:
client = self.client()
api_instance = VersionApi(client)
version: VersionInfo = api_instance.get_code()
return OutpostServiceConnectionState(version=version.git_version, healthy=True)
except (OpenApiException, HTTPError, ServiceConnectionInvalid):
return OutpostServiceConnectionState(version="", healthy=False)
def client(self) -> ApiClient:
"""Get Kubernetes client configured from kubeconfig"""
config = Configuration()
try:
if self.local:
load_incluster_config(client_configuration=config)
else:
load_kube_config_from_dict(self.kubeconfig, client_configuration=config)
return ApiClient(config)
except ConfigException as exc:
raise ServiceConnectionInvalid from exc
class Meta: class Meta:
verbose_name = _("Kubernetes Service-Connection") verbose_name = _("Kubernetes Service-Connection")

View File

@ -25,6 +25,8 @@ from authentik.events.monitored_tasks import (
) )
from authentik.lib.utils.reflection import path_to_class from authentik.lib.utils.reflection import path_to_class
from authentik.outposts.controllers.base import BaseController, ControllerException from authentik.outposts.controllers.base import BaseController, ControllerException
from authentik.outposts.controllers.docker import DockerClient
from authentik.outposts.controllers.kubernetes import KubernetesClient
from authentik.outposts.models import ( from authentik.outposts.models import (
DockerServiceConnection, DockerServiceConnection,
KubernetesServiceConnection, KubernetesServiceConnection,
@ -45,21 +47,21 @@ LOGGER = get_logger()
CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s" CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s"
def controller_for_outpost(outpost: Outpost) -> Optional[BaseController]: def controller_for_outpost(outpost: Outpost) -> Optional[type[BaseController]]:
"""Get a controller for the outpost, when a service connection is defined""" """Get a controller for the outpost, when a service connection is defined"""
if not outpost.service_connection: if not outpost.service_connection:
return None return None
service_connection = outpost.service_connection service_connection = outpost.service_connection
if outpost.type == OutpostType.PROXY: if outpost.type == OutpostType.PROXY:
if isinstance(service_connection, DockerServiceConnection): if isinstance(service_connection, DockerServiceConnection):
return ProxyDockerController(outpost, service_connection) return ProxyDockerController
if isinstance(service_connection, KubernetesServiceConnection): if isinstance(service_connection, KubernetesServiceConnection):
return ProxyKubernetesController(outpost, service_connection) return ProxyKubernetesController
if outpost.type == OutpostType.LDAP: if outpost.type == OutpostType.LDAP:
if isinstance(service_connection, DockerServiceConnection): if isinstance(service_connection, DockerServiceConnection):
return LDAPDockerController(outpost, service_connection) return LDAPDockerController
if isinstance(service_connection, KubernetesServiceConnection): if isinstance(service_connection, KubernetesServiceConnection):
return LDAPKubernetesController(outpost, service_connection) return LDAPKubernetesController
return None return None
@ -71,7 +73,12 @@ def outpost_service_connection_state(connection_pk: Any):
) )
if not connection: if not connection:
return return
state = connection.fetch_state() if isinstance(connection, DockerServiceConnection):
cls = DockerClient
if isinstance(connection, KubernetesServiceConnection):
cls = KubernetesClient
with cls(connection) as client:
state = client.fetch_state()
cache.set(connection.state_key, state, timeout=None) cache.set(connection.state_key, state, timeout=None)
@ -114,9 +121,10 @@ def outpost_controller(
return return
self.set_uid(slugify(outpost.name)) self.set_uid(slugify(outpost.name))
try: try:
controller = controller_for_outpost(outpost) controller_type = controller_for_outpost(outpost)
if not controller: if not controller_type:
return return
with controller_type(outpost, outpost.service_connection) as controller:
logs = getattr(controller, f"{action}_with_logs")() logs = getattr(controller, f"{action}_with_logs")()
LOGGER.debug("---------------Outpost Controller logs starting----------------") LOGGER.debug("---------------Outpost Controller logs starting----------------")
for log in logs: for log in logs:

86
poetry.lock generated
View File

@ -175,6 +175,22 @@ GitPython = ">=1.0.1"
PyYAML = ">=5.3.1" PyYAML = ">=5.3.1"
stevedore = ">=1.20.0" stevedore = ">=1.20.0"
[[package]]
name = "bcrypt"
version = "3.2.0"
description = "Modern password hashing for your software and your servers"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
cffi = ">=1.1"
six = ">=1.4.1"
[package.extras]
tests = ["pytest (>=3.2.1,!=3.3.0)"]
typecheck = ["mypy"]
[[package]] [[package]]
name = "billiard" name = "billiard"
version = "3.6.4.0" version = "3.6.4.0"
@ -1162,6 +1178,25 @@ python-versions = ">=3.6"
[package.dependencies] [package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "paramiko"
version = "2.9.1"
description = "SSH2 protocol library"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
bcrypt = ">=3.1.3"
cryptography = ">=2.5"
pynacl = ">=1.0.1"
[package.extras]
all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"]
ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"]
gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"]
invoke = ["invoke (>=1.3)"]
[[package]] [[package]]
name = "pathspec" name = "pathspec"
version = "0.9.0" version = "0.9.0"
@ -1332,6 +1367,22 @@ python-versions = "*"
[package.dependencies] [package.dependencies]
pylint = ">=1.7" pylint = ">=1.7"
[[package]]
name = "pynacl"
version = "1.4.0"
description = "Python binding to the Networking and Cryptography (NaCl) library"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
cffi = ">=1.4.1"
six = "*"
[package.extras]
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"]
[[package]] [[package]]
name = "pyopenssl" name = "pyopenssl"
version = "21.0.0" version = "21.0.0"
@ -2024,7 +2075,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "2caacecbae1850c6cd20e52ce70723b2e21e57cc54a9a3cd9dd8e00e6a6da481" content-hash = "39b437e0cbd49396c867f8a7cfd3d0581facfbb830e069f758c71c89be09d1f6"
[metadata.files] [metadata.files]
aiohttp = [ aiohttp = [
@ -2152,6 +2203,15 @@ bandit = [
{file = "bandit-1.7.1-py3-none-any.whl", hash = "sha256:f5acd838e59c038a159b5c621cf0f8270b279e884eadd7b782d7491c02add0d4"}, {file = "bandit-1.7.1-py3-none-any.whl", hash = "sha256:f5acd838e59c038a159b5c621cf0f8270b279e884eadd7b782d7491c02add0d4"},
{file = "bandit-1.7.1.tar.gz", hash = "sha256:a81b00b5436e6880fa8ad6799bc830e02032047713cbb143a12939ac67eb756c"}, {file = "bandit-1.7.1.tar.gz", hash = "sha256:a81b00b5436e6880fa8ad6799bc830e02032047713cbb143a12939ac67eb756c"},
] ]
bcrypt = [
{file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"},
{file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"},
{file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"},
{file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"},
{file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"},
{file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"},
{file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"},
]
billiard = [ billiard = [
{file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"}, {file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"},
{file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"}, {file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"},
@ -2903,6 +2963,10 @@ packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
] ]
paramiko = [
{file = "paramiko-2.9.1-py2.py3-none-any.whl", hash = "sha256:db5d3f19607941b1c90233588d60213c874392c4961c6297037da989c24f8070"},
{file = "paramiko-2.9.1.tar.gz", hash = "sha256:a1fdded3b55f61d23389e4fe52d9ae428960ac958d2edf50373faa5d8926edd0"},
]
pathspec = [ pathspec = [
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
@ -3051,6 +3115,26 @@ pylint-plugin-utils = [
{file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"}, {file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"},
{file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"}, {file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"},
] ]
pynacl = [
{file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"},
{file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"},
{file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"},
{file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"},
{file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"},
{file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"},
{file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"},
{file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"},
{file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"},
{file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"},
{file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"},
{file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"},
{file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"},
{file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"},
{file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"},
{file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"},
{file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"},
{file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"},
]
pyopenssl = [ pyopenssl = [
{file = "pyOpenSSL-21.0.0-py2.py3-none-any.whl", hash = "sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6"}, {file = "pyOpenSSL-21.0.0-py2.py3-none-any.whl", hash = "sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6"},
{file = "pyOpenSSL-21.0.0.tar.gz", hash = "sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3"}, {file = "pyOpenSSL-21.0.0.tar.gz", hash = "sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3"},

View File

@ -145,6 +145,7 @@ webauthn = "*"
xmlsec = "*" xmlsec = "*"
flower = "*" flower = "*"
wsproto = "*" wsproto = "*"
paramiko = "^2.9.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
bandit = "*" bandit = "*"

View File

@ -666,8 +666,12 @@ msgid "Callback URL"
msgstr "Callback URL" msgstr "Callback URL"
#: src/pages/outposts/ServiceConnectionDockerForm.ts #: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." #~ msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system."
msgstr "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." #~ msgstr "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system."
#: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system."
msgstr "Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system."
#: src/elements/forms/ConfirmationForm.ts #: src/elements/forms/ConfirmationForm.ts
#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteBulkForm.ts
@ -4885,8 +4889,12 @@ msgid "System task execution"
msgstr "System task execution" msgstr "System task execution"
#: src/pages/outposts/ServiceConnectionDockerForm.ts #: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "TLS Authentication Certificate" #~ msgid "TLS Authentication Certificate"
msgstr "TLS Authentication Certificate" #~ msgstr "TLS Authentication Certificate"
#: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "TLS Authentication Certificate/SSH Keypair"
msgstr "TLS Authentication Certificate/SSH Keypair"
#: #:
#~ msgid "TLS Server name" #~ msgid "TLS Server name"
@ -5879,6 +5887,10 @@ msgstr "When a valid username/email has been entered, and this option is enabled
msgid "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate." msgid "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate."
msgstr "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate." msgstr "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate."
#: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "When connecting via SSH, this keypair is used for authentication."
msgstr "When connecting via SSH, this keypair is used for authentication."
#: src/pages/stages/email/EmailStageForm.ts #: src/pages/stages/email/EmailStageForm.ts
msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored."
msgstr "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgstr "When enabled, global Email connection settings will be used and connection settings below will be ignored."

View File

@ -669,8 +669,12 @@ msgid "Callback URL"
msgstr "URL de rappel" msgstr "URL de rappel"
#: src/pages/outposts/ServiceConnectionDockerForm.ts #: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." #~ msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system."
msgstr "Peut être au format \"unix://\" pour une connexion à un service docker local, ou \"https://:2376\" pour une connexion à un système distant." #~ msgstr "Peut être au format \"unix://\" pour une connexion à un service docker local, ou \"https://:2376\" pour une connexion à un système distant."
#: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system."
msgstr ""
#: src/elements/forms/ConfirmationForm.ts #: src/elements/forms/ConfirmationForm.ts
#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteBulkForm.ts
@ -4841,8 +4845,12 @@ msgid "System task execution"
msgstr "Exécution de tâche système" msgstr "Exécution de tâche système"
#: src/pages/outposts/ServiceConnectionDockerForm.ts #: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "TLS Authentication Certificate" #~ msgid "TLS Authentication Certificate"
msgstr "Certificat TLS d'authentification" #~ msgstr "Certificat TLS d'authentification"
#: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "TLS Authentication Certificate/SSH Keypair"
msgstr ""
#~ msgid "TLS Server name" #~ msgid "TLS Server name"
#~ msgstr "Nom TLS du serveur" #~ msgstr "Nom TLS du serveur"
@ -5818,6 +5826,10 @@ msgstr "Lorsqu'un nom d'utilisateur/email valide a été saisi, et si cette opti
msgid "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate." msgid "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate."
msgstr "" msgstr ""
#: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "When connecting via SSH, this keypair is used for authentication."
msgstr ""
#: src/pages/stages/email/EmailStageForm.ts #: src/pages/stages/email/EmailStageForm.ts
msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored."
msgstr "Si activé, les paramètres globaux de connexion courriel seront utilisés et les paramètres de connexion ci-dessous seront ignorés." msgstr "Si activé, les paramètres globaux de connexion courriel seront utilisés et les paramètres de connexion ci-dessous seront ignorés."

View File

@ -662,7 +662,11 @@ msgid "Callback URL"
msgstr "" msgstr ""
#: src/pages/outposts/ServiceConnectionDockerForm.ts #: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." #~ msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system."
#~ msgstr ""
#: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system."
msgstr "" msgstr ""
#: src/elements/forms/ConfirmationForm.ts #: src/elements/forms/ConfirmationForm.ts
@ -4875,7 +4879,11 @@ msgid "System task execution"
msgstr "" msgstr ""
#: src/pages/outposts/ServiceConnectionDockerForm.ts #: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "TLS Authentication Certificate" #~ msgid "TLS Authentication Certificate"
#~ msgstr ""
#: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "TLS Authentication Certificate/SSH Keypair"
msgstr "" msgstr ""
#: #:
@ -5859,6 +5867,10 @@ msgstr ""
msgid "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate." msgid "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate."
msgstr "" msgstr ""
#: src/pages/outposts/ServiceConnectionDockerForm.ts
msgid "When connecting via SSH, this keypair is used for authentication."
msgstr ""
#: src/pages/stages/email/EmailStageForm.ts #: src/pages/stages/email/EmailStageForm.ts
msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored." msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored."
msgstr "" msgstr ""

View File

@ -72,7 +72,7 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
required required
/> />
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system.`} ${t`Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system.`}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
@ -106,7 +106,7 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${t`TLS Authentication Certificate`} label=${t`TLS Authentication Certificate/SSH Keypair`}
name="tlsAuthentication" name="tlsAuthentication"
> >
<select class="pf-c-form-control"> <select class="pf-c-form-control">
@ -134,6 +134,9 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
<p class="pf-c-form__helper-text"> <p class="pf-c-form__helper-text">
${t`Certificate/Key used for authentication. Can be left empty for no authentication.`} ${t`Certificate/Key used for authentication. Can be left empty for no authentication.`}
</p> </p>
<p class="pf-c-form__helper-text">
${t`When connecting via SSH, this keypair is used for authentication.`}
</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
</form>`; </form>`;
} }

View File

@ -39,7 +39,7 @@ To minimise the potential risks of mapping the docker socket into a container/gi
- Containers/Kill: Cleanup during upgrades - Containers/Kill: Cleanup during upgrades
- Containers/Remove: Removal of outposts - Containers/Remove: Removal of outposts
## Remote hosts ## Remote hosts (TLS)
To connect remote hosts, you can follow this Guide from Docker [Use TLS (HTTPS) to protect the Docker daemon socket](https://docs.docker.com/engine/security/protect-access/#use-tls-https-to-protect-the-docker-daemon-socket) to configure Docker. To connect remote hosts, you can follow this Guide from Docker [Use TLS (HTTPS) to protect the Docker daemon socket](https://docs.docker.com/engine/security/protect-access/#use-tls-https-to-protect-the-docker-daemon-socket) to configure Docker.
@ -49,3 +49,25 @@ Afterwards, create two Certificate-keypairs in authentik:
- `Docker Cert`, with the contents of `~/.docker/cert.pem` as Certificate and `~/.docker/key.pem` as Private key. - `Docker Cert`, with the contents of `~/.docker/cert.pem` as Certificate and `~/.docker/key.pem` as Private key.
Create an integration with `Docker CA` as *TLS Verification Certificate* and `Docker Cert` as *TLS Authentication Certificate*. Create an integration with `Docker CA` as *TLS Verification Certificate* and `Docker Cert` as *TLS Authentication Certificate*.
## Remote hosts (SSH)
Starting with authentik 2021.12.5, you can connect to remote docker hosts using SSH. To configure this, create a new SSH keypair using these commands:
```
# Generate the keypair itself, using RSA keys in the PEM format
ssh-keygen -t rsa -f authentik -N "" -m pem
# Generate a certificate from the private key, required by authentik.
# The values that openssl prompts you for are not relevant
openssl req -x509 -sha256 -nodes -days 365 -out certificate.pem -key authentik
```
You'll end up with three files:
- `authentik.pub` is the public key, this should be added to the `~/.ssh/authorized_keys` file on the target host and user.
- `authentik` is the private key, which should be imported into a Keypair in authentik.
- `certificate.pem` is the matching certificate for the keypair above.
Modify/create a new Docker integration, and set your *Docker URL* to `ssh://hostname`, and select the keypair you created above as *TLS Authentication Certificate/SSH Keypair*.
The *Docker URL* field include a user, if none is specified authentik connects with the user `authentik`.