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:
Jens Langhammer 2021-12-03 18:27:06 +01:00
parent 8db68410c6
commit 572f6d4ea0
14 changed files with 209 additions and 4 deletions

View File

@ -20,6 +20,7 @@ from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.builder import CertificateBuilder from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.managed import MANAGED_KEY
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
@ -141,7 +142,7 @@ class CertificateKeyPairFilter(FilterSet):
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
"""CertificateKeyPair Viewset""" """CertificateKeyPair Viewset"""
queryset = CertificateKeyPair.objects.exclude(managed__isnull=False) queryset = CertificateKeyPair.objects.exclude(managed=MANAGED_KEY)
serializer_class = CertificateKeyPairSerializer serializer_class = CertificateKeyPairSerializer
filterset_class = CertificateKeyPairFilter filterset_class = CertificateKeyPairFilter
ordering = ["name"] ordering = ["name"]

View File

@ -13,3 +13,4 @@ class AuthentikCryptoConfig(AppConfig):
def ready(self): def ready(self):
import_module("authentik.crypto.managed") import_module("authentik.crypto.managed")
import_module("authentik.crypto.tasks")

View File

@ -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"},
},
}

67
authentik/crypto/tasks.py Normal file
View File

@ -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})],
)
)

View File

@ -1,5 +1,7 @@
"""Crypto tests""" """Crypto tests"""
import datetime import datetime
from os import makedirs, mkdir
from tempfile import TemporaryDirectory
from django.urls import reverse from django.urls import reverse
from rest_framework.test import APITestCase 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.api import CertificateKeyPairSerializer
from authentik.crypto.builder import CertificateBuilder from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair 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.lib.generators import generate_key
from authentik.providers.oauth2.models import OAuth2Provider 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()
)

View File

@ -83,3 +83,4 @@ default_user_change_email: true
default_user_change_username: true default_user_change_username: true
gdpr_compliance: true gdpr_compliance: true
cert_discovery_dir: /certs

View File

@ -55,6 +55,7 @@ services:
volumes: volumes:
- ./backups:/backups - ./backups:/backups
- ./media:/media - ./media:/media
- ./certs:/certs
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./custom-templates:/templates - ./custom-templates:/templates
- geoip:/geoip - geoip:/geoip

View File

@ -28,7 +28,7 @@ function check_if_root {
GROUP="authentik:${GROUP_NAME}" GROUP="authentik:${GROUP_NAME}"
fi fi
# Fix permissions of backups and media # 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 chpst -u authentik:$GROUP env HOME=/authentik $1
} }

View File

@ -2705,6 +2705,14 @@ msgstr "MFA Devices"
msgid "Make sure to keep these tokens in a safe place." msgid "Make sure to keep these tokens in a safe place."
msgstr "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 #: src/pages/stages/user_write/UserWriteStageForm.ts
msgid "Mark newly created users as inactive." msgid "Mark newly created users as inactive."
msgstr "Mark newly created users as inactive." msgstr "Mark newly created users as inactive."

View File

@ -2686,6 +2686,14 @@ msgstr ""
msgid "Make sure to keep these tokens in a safe place." msgid "Make sure to keep these tokens in a safe place."
msgstr "" 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 #: src/pages/stages/user_write/UserWriteStageForm.ts
msgid "Mark newly created users as inactive." msgid "Mark newly created users as inactive."
msgstr "Marquer les utilisateurs nouvellements créés comme inactifs." msgstr "Marquer les utilisateurs nouvellements créés comme inactifs."

View File

@ -2697,6 +2697,14 @@ msgstr ""
msgid "Make sure to keep these tokens in a safe place." msgid "Make sure to keep these tokens in a safe place."
msgstr "" 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 #: src/pages/stages/user_write/UserWriteStageForm.ts
msgid "Mark newly created users as inactive." msgid "Mark newly created users as inactive."
msgstr "" msgstr ""

View File

@ -91,8 +91,13 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
} }
row(item: CertificateKeyPair): TemplateResult[] { 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 [ 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}> html`<ak-label color=${item.privateKeyAvailable ? PFColor.Green : PFColor.Grey}>
${item.privateKeyAvailable ? t`Yes` : t`No`} ${item.privateKeyAvailable ? t`Yes` : t`No`}
</ak-label>`, </ak-label>`,

View File

@ -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.

View File

@ -22,7 +22,12 @@ module.exports = {
type: "category", type: "category",
label: "Core Concepts", label: "Core Concepts",
collapsed: false, collapsed: false,
items: ["core/terminology", "core/applications", "core/tenants"], items: [
"core/terminology",
"core/applications",
"core/tenants",
"core/certificates",
],
}, },
{ {
type: "category", type: "category",