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.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"]

View File

@ -13,3 +13,4 @@ class AuthentikCryptoConfig(AppConfig):
def ready(self):
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"""
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()
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>`,

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",
label: "Core Concepts",
collapsed: false,
items: ["core/terminology", "core/applications", "core/tenants"],
items: [
"core/terminology",
"core/applications",
"core/tenants",
"core/certificates",
],
},
{
type: "category",