crypto: add certificate discovery to automatically import certificates from lets encrypt
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> #1835
This commit is contained in:
parent
8db68410c6
commit
572f6d4ea0
|
@ -20,6 +20,7 @@ from authentik.api.decorators import permission_required
|
|||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.managed import MANAGED_KEY
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
|
@ -141,7 +142,7 @@ class CertificateKeyPairFilter(FilterSet):
|
|||
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
"""CertificateKeyPair Viewset"""
|
||||
|
||||
queryset = CertificateKeyPair.objects.exclude(managed__isnull=False)
|
||||
queryset = CertificateKeyPair.objects.exclude(managed=MANAGED_KEY)
|
||||
serializer_class = CertificateKeyPairSerializer
|
||||
filterset_class = CertificateKeyPairFilter
|
||||
ordering = ["name"]
|
||||
|
|
|
@ -13,3 +13,4 @@ class AuthentikCryptoConfig(AppConfig):
|
|||
|
||||
def ready(self):
|
||||
import_module("authentik.crypto.managed")
|
||||
import_module("authentik.crypto.tasks")
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
"""Crypto task Settings"""
|
||||
from celery.schedules import crontab
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"crypto_certificate_discovery": {
|
||||
"task": "authentik.crypto.tasks.certificate_discovery",
|
||||
"schedule": crontab(minute="*/5"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
"""Crypto tasks"""
|
||||
from glob import glob
|
||||
from pathlib import Path
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.monitored_tasks import PrefilledMonitoredTask, TaskResult, TaskResultStatus
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
MANAGED_DISCOVERED = "goauthentik.io/crypto/discovered/%s"
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=PrefilledMonitoredTask)
|
||||
def certificate_discovery(self: PrefilledMonitoredTask):
|
||||
"""Discover and update certificates form the filesystem"""
|
||||
certs = {}
|
||||
private_keys = {}
|
||||
discovered = 0
|
||||
for file in glob(CONFIG.y("cert_discovery_dir") + "/**", recursive=True):
|
||||
path = Path(file)
|
||||
if not path.exists():
|
||||
continue
|
||||
if path.is_dir():
|
||||
continue
|
||||
# Support certbot's directory structure
|
||||
if path.name in ["fullchain.pem", "privkey.pem"]:
|
||||
cert_name = path.parent.name
|
||||
else:
|
||||
cert_name = path.name.replace(path.suffix, "")
|
||||
try:
|
||||
with open(path, "r+", encoding="utf-8") as _file:
|
||||
body = _file.read()
|
||||
if "BEGIN RSA PRIVATE KEY" in body:
|
||||
private_keys[cert_name] = body
|
||||
else:
|
||||
certs[cert_name] = body
|
||||
except OSError as exc:
|
||||
LOGGER.warning("Failed to open file", exc=exc, file=path)
|
||||
discovered += 1
|
||||
for name, cert_data in certs.items():
|
||||
cert = CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % name).first()
|
||||
if not cert:
|
||||
cert = CertificateKeyPair(
|
||||
name=name,
|
||||
managed=MANAGED_DISCOVERED % name,
|
||||
)
|
||||
dirty = False
|
||||
if cert.certificate_data != cert_data:
|
||||
cert.certificate_data = cert_data
|
||||
dirty = True
|
||||
if name in private_keys:
|
||||
if cert.key_data == private_keys[name]:
|
||||
cert.key_data = private_keys[name]
|
||||
dirty = True
|
||||
if dirty:
|
||||
cert.save()
|
||||
self.set_status(
|
||||
TaskResult(
|
||||
TaskResultStatus.SUCCESSFUL,
|
||||
messages=[_("Successfully imported %(count)d files." % {"count": discovered})],
|
||||
)
|
||||
)
|
|
@ -1,5 +1,7 @@
|
|||
"""Crypto tests"""
|
||||
import datetime
|
||||
from os import makedirs, mkdir
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
@ -9,6 +11,8 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_cert,
|
|||
from authentik.crypto.api import CertificateKeyPairSerializer
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
|
||||
|
@ -163,3 +167,32 @@ class TestCrypto(APITestCase):
|
|||
}
|
||||
],
|
||||
)
|
||||
|
||||
def test_discovery(self):
|
||||
"""Test certificate discovery"""
|
||||
builder = CertificateBuilder()
|
||||
builder.common_name = "test-cert"
|
||||
with self.assertRaises(ValueError):
|
||||
builder.save()
|
||||
builder.build(
|
||||
subject_alt_names=[],
|
||||
validity_days=3,
|
||||
)
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
with open(f"{temp_dir}/foo.pem", "w+", encoding="utf-8") as _cert:
|
||||
_cert.write(builder.certificate)
|
||||
with open(f"{temp_dir}/foo.key", "w+", encoding="utf-8") as _key:
|
||||
_key.write(builder.private_key)
|
||||
makedirs(f"{temp_dir}/foo.bar", exist_ok=True)
|
||||
with open(f"{temp_dir}/foo.bar/fullchain.pem", "w+", encoding="utf-8") as _cert:
|
||||
_cert.write(builder.certificate)
|
||||
with open(f"{temp_dir}/foo.bar/privkey.pem", "w+", encoding="utf-8") as _key:
|
||||
_key.write(builder.private_key)
|
||||
with CONFIG.patch("cert_discovery_dir", temp_dir):
|
||||
certificate_discovery()
|
||||
self.assertTrue(
|
||||
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo").exists()
|
||||
)
|
||||
self.assertTrue(
|
||||
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo.bar").exists()
|
||||
)
|
||||
|
|
|
@ -83,3 +83,4 @@ default_user_change_email: true
|
|||
default_user_change_username: true
|
||||
|
||||
gdpr_compliance: true
|
||||
cert_discovery_dir: /certs
|
||||
|
|
|
@ -55,6 +55,7 @@ services:
|
|||
volumes:
|
||||
- ./backups:/backups
|
||||
- ./media:/media
|
||||
- ./certs:/certs
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./custom-templates:/templates
|
||||
- geoip:/geoip
|
||||
|
|
|
@ -28,7 +28,7 @@ function check_if_root {
|
|||
GROUP="authentik:${GROUP_NAME}"
|
||||
fi
|
||||
# Fix permissions of backups and media
|
||||
chown -R authentik:authentik /media /backups
|
||||
chown -R authentik:authentik /media /backups /certs
|
||||
chpst -u authentik:$GROUP env HOME=/authentik $1
|
||||
}
|
||||
|
||||
|
|
|
@ -2705,6 +2705,14 @@ msgstr "MFA Devices"
|
|||
msgid "Make sure to keep these tokens in a safe place."
|
||||
msgstr "Make sure to keep these tokens in a safe place."
|
||||
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
msgid "Managed by authentik"
|
||||
msgstr "Managed by authentik"
|
||||
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
msgid "Managed by authentik (Discovered)"
|
||||
msgstr "Managed by authentik (Discovered)"
|
||||
|
||||
#: src/pages/stages/user_write/UserWriteStageForm.ts
|
||||
msgid "Mark newly created users as inactive."
|
||||
msgstr "Mark newly created users as inactive."
|
||||
|
|
|
@ -2686,6 +2686,14 @@ msgstr ""
|
|||
msgid "Make sure to keep these tokens in a safe place."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
msgid "Managed by authentik"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
msgid "Managed by authentik (Discovered)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/user_write/UserWriteStageForm.ts
|
||||
msgid "Mark newly created users as inactive."
|
||||
msgstr "Marquer les utilisateurs nouvellements créés comme inactifs."
|
||||
|
|
|
@ -2697,6 +2697,14 @@ msgstr ""
|
|||
msgid "Make sure to keep these tokens in a safe place."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
msgid "Managed by authentik"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/crypto/CertificateKeyPairListPage.ts
|
||||
msgid "Managed by authentik (Discovered)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/user_write/UserWriteStageForm.ts
|
||||
msgid "Mark newly created users as inactive."
|
||||
msgstr ""
|
||||
|
|
|
@ -91,8 +91,13 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
|
|||
}
|
||||
|
||||
row(item: CertificateKeyPair): TemplateResult[] {
|
||||
let managedSubText = t`Managed by authentik`;
|
||||
if (item.managed && item.managed.startsWith("goauthentik.io/crypto/discovered")) {
|
||||
managedSubText = t`Managed by authentik (Discovered)`;
|
||||
}
|
||||
return [
|
||||
html`${item.name}`,
|
||||
html`<div>${item.name}</div>
|
||||
${item.managed ? html`<small>${managedSubText}</small>` : html``}`,
|
||||
html`<ak-label color=${item.privateKeyAvailable ? PFColor.Green : PFColor.Grey}>
|
||||
${item.privateKeyAvailable ? t`Yes` : t`No`}
|
||||
</ak-label>`,
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
title: Certificates
|
||||
---
|
||||
|
||||
Certificates in authentik are used for the following use cases:
|
||||
|
||||
- Signing and verifying SAML Requests and Responses
|
||||
- Signing JSON Web Tokens for OAuth and OIDC
|
||||
- Connecting to remote docker hosts using the Docker integration
|
||||
- Verifying LDAP Servers' certificates
|
||||
- Encrypting outposts's endpoints
|
||||
|
||||
## Default certificate
|
||||
|
||||
Every authentik install generates a self-signed certificate on the first start. The certificate is called *authentik Self-signed Certificate* and is valid for 1 year.
|
||||
|
||||
This certificate is generated to be used as a default for all OAuth2/OIDC providers, as these don't require the certificate to be configured on both sides (the signature of a JWT is validated using the [JWKS](https://auth0.com/docs/security/tokens/json-web-tokens/json-web-key-sets) URL).
|
||||
|
||||
This certificate can also be used for SAML Providers/Sources, just keep in mind that the certificate is only valid for a year. Some SAML applications require the certificate to be valid, so they might need to be rotated regularly.
|
||||
|
||||
For SAML use-cases, you can generate a Certificate thats valid for longer than 1 year, on your own risk.
|
||||
|
||||
## External certificates
|
||||
|
||||
To use externally managed certificates, for example generated with certbot or HashiCorp Vault, you can use the discovery feature.
|
||||
|
||||
The docker-compose installation maps a `certs` directory to `/certs`, you can simply use this as an output directory for certbot.
|
||||
|
||||
For Kubernetes, you can map custom secrets/volumes under `/certs`.
|
||||
|
||||
You can also bind mount single files into the folder, as long as they fall under this naming schema.
|
||||
|
||||
- Files in the root directory will be imported based on their filename.
|
||||
|
||||
`/foo.pem` Will be imported as the keypair `foo`. Based on its content its either imported as certificate or private key.
|
||||
|
||||
Currently, only RSA Keys are supported, so if the file contains `BEGIN RSA PRIVATE KEY` it will imported as private key.
|
||||
|
||||
Otherwise it will be imported as certificate.
|
||||
|
||||
- If the file is called `fullchain.pem` or `privkey.pem` (the output naming of certbot), they will get the name of the parent folder.
|
||||
- Files can be in any arbitrary file structure, and can have extension.
|
||||
|
||||
```
|
||||
certs/
|
||||
├── baz
|
||||
│ └── bar.baz
|
||||
│ ├── fullchain.pem
|
||||
│ └── privkey.key
|
||||
├── foo.bar
|
||||
│ ├── fullchain.pem
|
||||
│ └── privkey.key
|
||||
├── foo.key
|
||||
└── foo.pem
|
||||
```
|
||||
|
||||
Files are checked every 5 minutes, and will trigger an Outpost refresh if the files differ.
|
|
@ -22,7 +22,12 @@ module.exports = {
|
|||
type: "category",
|
||||
label: "Core Concepts",
|
||||
collapsed: false,
|
||||
items: ["core/terminology", "core/applications", "core/tenants"],
|
||||
items: [
|
||||
"core/terminology",
|
||||
"core/applications",
|
||||
"core/tenants",
|
||||
"core/certificates",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
|
|
Reference in New Issue