outposts: add remote docker integration via SSH
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
19b707a0fb
commit
6510b97c1e
|
@ -9,7 +9,11 @@ from structlog.testing import capture_logs
|
|||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
from authentik.lib.config import CONFIG
|
||||
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"
|
||||
|
||||
|
@ -28,11 +32,25 @@ class DeploymentPort:
|
|||
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:
|
||||
"""Base Outpost deployment controller"""
|
||||
|
||||
deployment_ports: list[DeploymentPort]
|
||||
|
||||
client: BaseClient
|
||||
outpost: Outpost
|
||||
connection: OutpostServiceConnection
|
||||
|
||||
|
@ -63,6 +81,13 @@ class BaseController:
|
|||
self.down()
|
||||
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:
|
||||
"""Return a static deployment configuration"""
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -1,17 +1,75 @@
|
|||
"""Docker controller"""
|
||||
from time import sleep
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.text import slugify
|
||||
from docker import DockerClient
|
||||
from docker import DockerClient as UpstreamDockerClient
|
||||
from docker.errors import DockerException, NotFound
|
||||
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 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.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):
|
||||
|
@ -27,8 +85,9 @@ class DockerController(BaseController):
|
|||
if outpost.managed == MANAGED_OUTPOST:
|
||||
return
|
||||
try:
|
||||
self.client = connection.client()
|
||||
except ServiceConnectionInvalid as exc:
|
||||
self.client = DockerClient(connection)
|
||||
except DockerException as exc:
|
||||
self.logger.warning(exc)
|
||||
raise ControllerException from exc
|
||||
|
||||
@property
|
||||
|
|
|
@ -2,19 +2,53 @@
|
|||
from io import StringIO
|
||||
from typing import Type
|
||||
|
||||
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 structlog.testing import capture_logs
|
||||
from urllib3.exceptions import HTTPError
|
||||
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.deployment import DeploymentReconciler
|
||||
from authentik.outposts.controllers.k8s.secret import SecretReconciler
|
||||
from authentik.outposts.controllers.k8s.service import ServiceReconciler
|
||||
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):
|
||||
|
@ -23,12 +57,12 @@ class KubernetesController(BaseController):
|
|||
reconcilers: dict[str, Type[KubernetesObjectReconciler]]
|
||||
reconcile_order: list[str]
|
||||
|
||||
client: ApiClient
|
||||
client: KubernetesClient
|
||||
connection: KubernetesServiceConnection
|
||||
|
||||
def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection) -> None:
|
||||
super().__init__(outpost, connection)
|
||||
self.client = connection.client()
|
||||
self.client = KubernetesClient(connection)
|
||||
self.reconcilers = {
|
||||
"secret": SecretReconciler,
|
||||
"deployment": DeploymentReconciler,
|
||||
|
|
|
@ -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)
|
|
@ -1,4 +1,5 @@
|
|||
"""Create Docker TLSConfig from CertificateKeyPair"""
|
||||
from os import unlink
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
from typing import Optional
|
||||
|
@ -14,6 +15,8 @@ class DockerInlineTLS:
|
|||
verification_kp: Optional[CertificateKeyPair]
|
||||
authentication_kp: Optional[CertificateKeyPair]
|
||||
|
||||
_paths: list[str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
verification_kp: Optional[CertificateKeyPair],
|
||||
|
@ -21,14 +24,21 @@ class DockerInlineTLS:
|
|||
) -> None:
|
||||
self.verification_kp = verification_kp
|
||||
self.authentication_kp = authentication_kp
|
||||
self._paths = []
|
||||
|
||||
def write_file(self, name: str, contents: str) -> str:
|
||||
"""Wrapper for mkstemp that uses fdopen"""
|
||||
path = Path(gettempdir(), name)
|
||||
with open(path, "w", encoding="utf8") as _file:
|
||||
_file.write(contents)
|
||||
self._paths.append(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:
|
||||
"""Create TLSConfig with Certificate Key pairs"""
|
||||
# So yes, this is quite ugly. But sadly, there is no clean way to pass
|
||||
|
|
|
@ -11,21 +11,11 @@ from django.core.cache import cache
|
|||
from django.db import IntegrityError, models, transaction
|
||||
from django.db.models.base import Model
|
||||
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.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 packaging.version import LegacyVersion, Version, parse
|
||||
from structlog.stdlib import get_logger
|
||||
from urllib3.exceptions import HTTPError
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
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.managed.models import ManagedModel
|
||||
from authentik.outposts.controllers.k8s.utils import get_namespace
|
||||
from authentik.outposts.docker_tls import DockerInlineTLS
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
OUR_VERSION = parse(__version__)
|
||||
|
@ -150,10 +139,6 @@ class OutpostServiceConnection(models.Model):
|
|||
return OutpostServiceConnectionState("", False)
|
||||
return state
|
||||
|
||||
def fetch_state(self) -> OutpostServiceConnectionState:
|
||||
"""Fetch current Service Connection state"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
"""Return component used to edit this object"""
|
||||
|
@ -211,35 +196,6 @@ class DockerServiceConnection(OutpostServiceConnection):
|
|||
def __str__(self) -> str:
|
||||
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:
|
||||
|
||||
verbose_name = _("Docker Service-Connection")
|
||||
|
@ -266,27 +222,6 @@ class KubernetesServiceConnection(OutpostServiceConnection):
|
|||
def __str__(self) -> str:
|
||||
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:
|
||||
|
||||
verbose_name = _("Kubernetes Service-Connection")
|
||||
|
|
|
@ -25,6 +25,8 @@ from authentik.events.monitored_tasks import (
|
|||
)
|
||||
from authentik.lib.utils.reflection import path_to_class
|
||||
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 (
|
||||
DockerServiceConnection,
|
||||
KubernetesServiceConnection,
|
||||
|
@ -45,21 +47,21 @@ LOGGER = get_logger()
|
|||
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"""
|
||||
if not outpost.service_connection:
|
||||
return None
|
||||
service_connection = outpost.service_connection
|
||||
if outpost.type == OutpostType.PROXY:
|
||||
if isinstance(service_connection, DockerServiceConnection):
|
||||
return ProxyDockerController(outpost, service_connection)
|
||||
return ProxyDockerController
|
||||
if isinstance(service_connection, KubernetesServiceConnection):
|
||||
return ProxyKubernetesController(outpost, service_connection)
|
||||
return ProxyKubernetesController
|
||||
if outpost.type == OutpostType.LDAP:
|
||||
if isinstance(service_connection, DockerServiceConnection):
|
||||
return LDAPDockerController(outpost, service_connection)
|
||||
return LDAPDockerController
|
||||
if isinstance(service_connection, KubernetesServiceConnection):
|
||||
return LDAPKubernetesController(outpost, service_connection)
|
||||
return LDAPKubernetesController
|
||||
return None
|
||||
|
||||
|
||||
|
@ -71,7 +73,12 @@ def outpost_service_connection_state(connection_pk: Any):
|
|||
)
|
||||
if not connection:
|
||||
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)
|
||||
|
||||
|
||||
|
@ -114,9 +121,10 @@ def outpost_controller(
|
|||
return
|
||||
self.set_uid(slugify(outpost.name))
|
||||
try:
|
||||
controller = controller_for_outpost(outpost)
|
||||
if not controller:
|
||||
controller_type = controller_for_outpost(outpost)
|
||||
if not controller_type:
|
||||
return
|
||||
with controller_type(outpost, outpost.service_connection) as controller:
|
||||
logs = getattr(controller, f"{action}_with_logs")()
|
||||
LOGGER.debug("---------------Outpost Controller logs starting----------------")
|
||||
for log in logs:
|
||||
|
|
|
@ -175,6 +175,22 @@ GitPython = ">=1.0.1"
|
|||
PyYAML = ">=5.3.1"
|
||||
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]]
|
||||
name = "billiard"
|
||||
version = "3.6.4.0"
|
||||
|
@ -1162,6 +1178,25 @@ python-versions = ">=3.6"
|
|||
[package.dependencies]
|
||||
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]]
|
||||
name = "pathspec"
|
||||
version = "0.9.0"
|
||||
|
@ -1332,6 +1367,22 @@ python-versions = "*"
|
|||
[package.dependencies]
|
||||
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]]
|
||||
name = "pyopenssl"
|
||||
version = "21.0.0"
|
||||
|
@ -2024,7 +2075,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "2caacecbae1850c6cd20e52ce70723b2e21e57cc54a9a3cd9dd8e00e6a6da481"
|
||||
content-hash = "39b437e0cbd49396c867f8a7cfd3d0581facfbb830e069f758c71c89be09d1f6"
|
||||
|
||||
[metadata.files]
|
||||
aiohttp = [
|
||||
|
@ -2152,6 +2203,15 @@ bandit = [
|
|||
{file = "bandit-1.7.1-py3-none-any.whl", hash = "sha256:f5acd838e59c038a159b5c621cf0f8270b279e884eadd7b782d7491c02add0d4"},
|
||||
{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 = [
|
||||
{file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"},
|
||||
{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.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 = [
|
||||
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
||||
{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-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 = [
|
||||
{file = "pyOpenSSL-21.0.0-py2.py3-none-any.whl", hash = "sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6"},
|
||||
{file = "pyOpenSSL-21.0.0.tar.gz", hash = "sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3"},
|
||||
|
|
|
@ -145,6 +145,7 @@ webauthn = "*"
|
|||
xmlsec = "*"
|
||||
flower = "*"
|
||||
wsproto = "*"
|
||||
paramiko = "^2.9.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
bandit = "*"
|
||||
|
|
|
@ -666,8 +666,12 @@ msgid "Callback URL"
|
|||
msgstr "Callback URL"
|
||||
|
||||
#: 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."
|
||||
msgstr "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."
|
||||
|
||||
#: 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/DeleteBulkForm.ts
|
||||
|
@ -4885,8 +4889,12 @@ msgid "System task execution"
|
|||
msgstr "System task execution"
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionDockerForm.ts
|
||||
msgid "TLS Authentication Certificate"
|
||||
msgstr "TLS Authentication Certificate"
|
||||
#~ msgid "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"
|
||||
|
@ -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."
|
||||
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
|
||||
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."
|
||||
|
|
|
@ -669,8 +669,12 @@ msgid "Callback URL"
|
|||
msgstr "URL de rappel"
|
||||
|
||||
#: 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."
|
||||
msgstr "Peut être au format \"unix://\" pour une connexion à un service docker local, ou \"https://:2376\" pour une connexion à un système distant."
|
||||
#~ 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."
|
||||
|
||||
#: 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/DeleteBulkForm.ts
|
||||
|
@ -4841,8 +4845,12 @@ msgid "System task execution"
|
|||
msgstr "Exécution de tâche système"
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionDockerForm.ts
|
||||
msgid "TLS Authentication Certificate"
|
||||
msgstr "Certificat TLS d'authentification"
|
||||
#~ msgid "TLS Authentication Certificate"
|
||||
#~ msgstr "Certificat TLS d'authentification"
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionDockerForm.ts
|
||||
msgid "TLS Authentication Certificate/SSH Keypair"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "TLS Server name"
|
||||
#~ 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."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionDockerForm.ts
|
||||
msgid "When connecting via SSH, this keypair is used for authentication."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/email/EmailStageForm.ts
|
||||
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."
|
||||
|
|
|
@ -662,7 +662,11 @@ msgid "Callback URL"
|
|||
msgstr ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: src/elements/forms/ConfirmationForm.ts
|
||||
|
@ -4875,7 +4879,11 @@ msgid "System task execution"
|
|||
msgstr ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#:
|
||||
|
@ -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."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionDockerForm.ts
|
||||
msgid "When connecting via SSH, this keypair is used for authentication."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/email/EmailStageForm.ts
|
||||
msgid "When enabled, global Email connection settings will be used and connection settings below will be ignored."
|
||||
msgstr ""
|
||||
|
|
|
@ -72,7 +72,7 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
|
|||
required
|
||||
/>
|
||||
<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>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
|
@ -106,7 +106,7 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
|
|||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`TLS Authentication Certificate`}
|
||||
label=${t`TLS Authentication Certificate/SSH Keypair`}
|
||||
name="tlsAuthentication"
|
||||
>
|
||||
<select class="pf-c-form-control">
|
||||
|
@ -134,6 +134,9 @@ export class ServiceConnectionDockerForm extends ModelForm<DockerServiceConnecti
|
|||
<p class="pf-c-form__helper-text">
|
||||
${t`Certificate/Key used for authentication. Can be left empty for no authentication.`}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`When connecting via SSH, this keypair is used for authentication.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</form>`;
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ To minimise the potential risks of mapping the docker socket into a container/gi
|
|||
- Containers/Kill: Cleanup during upgrades
|
||||
- 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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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`.
|
||||
|
|
Reference in New Issue