From 34b11524f15edcd2720135bf774f2eaa13e56008 Mon Sep 17 00:00:00 2001
From: Jens Langhammer
Date: Wed, 22 Dec 2021 11:43:45 +0100
Subject: [PATCH] tenants: add web certificate field, make authentik's core
certificate configurable based on keypair
Signed-off-by: Jens Langhammer
---
authentik/crypto/builder.py | 2 +-
authentik/outposts/models.py | 8 +-
authentik/outposts/signals.py | 2 +
authentik/tenants/api.py | 2 +
...01_squashed_0005_tenant_web_certificate.py | 146 ++++++++++++++++++
.../migrations/0005_tenant_web_certificate.py | 26 ++++
authentik/tenants/models.py | 9 ++
cmd/server/main.go | 7 +
internal/outpost/ak/http_tracing.go | 5 -
internal/web/tenant_tls/tenant_tls.go | 81 ++++++++++
internal/web/tls.go | 3 +
internal/web/web.go | 6 +-
schema.yml | 20 +++
web/src/locales/en.po | 5 +
web/src/locales/fr_FR.po | 5 +
web/src/locales/pseudo-LOCALE.po | 5 +
web/src/pages/tenants/TenantForm.ts | 38 ++++-
17 files changed, 358 insertions(+), 12 deletions(-)
create mode 100644 authentik/tenants/migrations/0001_squashed_0005_tenant_web_certificate.py
create mode 100644 authentik/tenants/migrations/0005_tenant_web_certificate.py
create mode 100644 internal/web/tenant_tls/tenant_tls.go
diff --git a/authentik/crypto/builder.py b/authentik/crypto/builder.py
index f78c8692f..2881750eb 100644
--- a/authentik/crypto/builder.py
+++ b/authentik/crypto/builder.py
@@ -44,7 +44,7 @@ class CertificateBuilder:
"""Build self-signed certificate"""
one_day = datetime.timedelta(1, 0, 0)
self.__private_key = rsa.generate_private_key(
- public_exponent=65537, key_size=2048, backend=default_backend()
+ public_exponent=65537, key_size=4096, backend=default_backend()
)
self.__public_key = self.__private_key.public_key()
alt_names: list[x509.GeneralName] = [x509.DNSName(x) for x in subject_alt_names or []]
diff --git a/authentik/outposts/models.py b/authentik/outposts/models.py
index a4300df19..7f73b4510 100644
--- a/authentik/outposts/models.py
+++ b/authentik/outposts/models.py
@@ -45,6 +45,7 @@ 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__)
OUTPOST_HELLO_INTERVAL = 10
@@ -385,7 +386,8 @@ class Outpost(ManagedModel):
user.user_permissions.add(permission.first())
LOGGER.debug(
"Updated service account's permissions",
- perms=UserObjectPermission.objects.filter(user=user),
+ obj_perms=UserObjectPermission.objects.filter(user=user),
+ perms=user.user_permissions.all(),
)
@property
@@ -449,6 +451,10 @@ class Outpost(ManagedModel):
objects.extend(provider.get_required_objects())
else:
objects.append(provider)
+ if self.managed:
+ for tenant in Tenant.objects.filter(web_certificate__isnull=False):
+ objects.append(tenant)
+ objects.append(tenant.web_certificate)
return objects
def __str__(self) -> str:
diff --git a/authentik/outposts/signals.py b/authentik/outposts/signals.py
index 025f8d970..7a1acba85 100644
--- a/authentik/outposts/signals.py
+++ b/authentik/outposts/signals.py
@@ -10,6 +10,7 @@ from authentik.crypto.models import CertificateKeyPair
from authentik.lib.utils.reflection import class_to_path
from authentik.outposts.models import Outpost, OutpostServiceConnection
from authentik.outposts.tasks import CACHE_KEY_OUTPOST_DOWN, outpost_controller, outpost_post_save
+from authentik.tenants.models import Tenant
LOGGER = get_logger()
UPDATE_TRIGGERING_MODELS = (
@@ -17,6 +18,7 @@ UPDATE_TRIGGERING_MODELS = (
OutpostServiceConnection,
Provider,
CertificateKeyPair,
+ Tenant,
)
diff --git a/authentik/tenants/api.py b/authentik/tenants/api.py
index 529fc74d4..a23d0b77a 100644
--- a/authentik/tenants/api.py
+++ b/authentik/tenants/api.py
@@ -39,6 +39,7 @@ class TenantSerializer(ModelSerializer):
"flow_recovery",
"flow_unenrollment",
"event_retention",
+ "web_certificate",
]
@@ -69,6 +70,7 @@ class TenantViewSet(UsedByMixin, ModelViewSet):
search_fields = [
"domain",
"branding_title",
+ "web_certificate__name",
]
filterset_fields = "__all__"
ordering = ["domain"]
diff --git a/authentik/tenants/migrations/0001_squashed_0005_tenant_web_certificate.py b/authentik/tenants/migrations/0001_squashed_0005_tenant_web_certificate.py
new file mode 100644
index 000000000..b5a6c2a3e
--- /dev/null
+++ b/authentik/tenants/migrations/0001_squashed_0005_tenant_web_certificate.py
@@ -0,0 +1,146 @@
+# Generated by Django 4.0 on 2021-12-22 09:42
+
+import uuid
+
+import django.db.models.deletion
+from django.apps.registry import Apps
+from django.db import migrations, models
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+
+import authentik.lib.utils.time
+
+
+def create_default_tenant(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
+ Flow = apps.get_model("authentik_flows", "Flow")
+ Tenant = apps.get_model("authentik_tenants", "Tenant")
+
+ db_alias = schema_editor.connection.alias
+
+ default_authentication = (
+ Flow.objects.using(db_alias).filter(slug="default-authentication-flow").first()
+ )
+ default_invalidation = (
+ Flow.objects.using(db_alias).filter(slug="default-invalidation-flow").first()
+ )
+
+ tenant, _ = Tenant.objects.using(db_alias).update_or_create(
+ domain="authentik-default",
+ default=True,
+ defaults={
+ "flow_authentication": default_authentication,
+ "flow_invalidation": default_invalidation,
+ },
+ )
+
+
+class Migration(migrations.Migration):
+
+ replaces = [
+ ("authentik_tenants", "0001_initial"),
+ ("authentik_tenants", "0002_default"),
+ ("authentik_tenants", "0003_tenant_branding_favicon"),
+ ("authentik_tenants", "0004_tenant_event_retention"),
+ ("authentik_tenants", "0005_tenant_web_certificate"),
+ ]
+
+ initial = True
+
+ dependencies = [
+ ("authentik_flows", "0018_oob_flows"),
+ ("authentik_flows", "0008_default_flows"),
+ ("authentik_crypto", "0003_certificatekeypair_managed"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Tenant",
+ fields=[
+ (
+ "tenant_uuid",
+ models.UUIDField(
+ default=uuid.uuid4, editable=False, primary_key=True, serialize=False
+ ),
+ ),
+ (
+ "domain",
+ models.TextField(
+ help_text="Domain that activates this tenant. Can be a superset, i.e. `a.b` for `aa.b` and `ba.b`"
+ ),
+ ),
+ ("default", models.BooleanField(default=False)),
+ ("branding_title", models.TextField(default="authentik")),
+ (
+ "branding_logo",
+ models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg"),
+ ),
+ (
+ "flow_authentication",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="tenant_authentication",
+ to="authentik_flows.flow",
+ ),
+ ),
+ (
+ "flow_invalidation",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="tenant_invalidation",
+ to="authentik_flows.flow",
+ ),
+ ),
+ (
+ "flow_recovery",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="tenant_recovery",
+ to="authentik_flows.flow",
+ ),
+ ),
+ (
+ "flow_unenrollment",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="tenant_unenrollment",
+ to="authentik_flows.flow",
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "Tenant",
+ "verbose_name_plural": "Tenants",
+ },
+ ),
+ migrations.RunPython(
+ code=create_default_tenant,
+ ),
+ migrations.AddField(
+ model_name="tenant",
+ name="branding_favicon",
+ field=models.TextField(default="/static/dist/assets/icons/icon.png"),
+ ),
+ migrations.AddField(
+ model_name="tenant",
+ name="event_retention",
+ field=models.TextField(
+ default="days=365",
+ help_text="Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).",
+ validators=[authentik.lib.utils.time.timedelta_string_validator],
+ ),
+ ),
+ migrations.AddField(
+ model_name="tenant",
+ name="web_certificate",
+ field=models.ForeignKey(
+ default=None,
+ help_text="Web Certificate used by the authentik Core webserver.",
+ null=True,
+ on_delete=django.db.models.deletion.SET_DEFAULT,
+ to="authentik_crypto.certificatekeypair",
+ ),
+ ),
+ ]
diff --git a/authentik/tenants/migrations/0005_tenant_web_certificate.py b/authentik/tenants/migrations/0005_tenant_web_certificate.py
new file mode 100644
index 000000000..14c247d55
--- /dev/null
+++ b/authentik/tenants/migrations/0005_tenant_web_certificate.py
@@ -0,0 +1,26 @@
+# Generated by Django 4.0 on 2021-12-22 09:42
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("authentik_crypto", "0003_certificatekeypair_managed"),
+ ("authentik_tenants", "0004_tenant_event_retention"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="tenant",
+ name="web_certificate",
+ field=models.ForeignKey(
+ default=None,
+ help_text="Web Certificate used by the authentik Core webserver.",
+ null=True,
+ on_delete=django.db.models.deletion.SET_DEFAULT,
+ to="authentik_crypto.certificatekeypair",
+ ),
+ ),
+ ]
diff --git a/authentik/tenants/models.py b/authentik/tenants/models.py
index 539cbc8f8..57830003e 100644
--- a/authentik/tenants/models.py
+++ b/authentik/tenants/models.py
@@ -4,6 +4,7 @@ from uuid import uuid4
from django.db import models
from django.utils.translation import gettext_lazy as _
+from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow
from authentik.lib.utils.time import timedelta_string_validator
@@ -51,6 +52,14 @@ class Tenant(models.Model):
),
)
+ web_certificate = models.ForeignKey(
+ CertificateKeyPair,
+ null=True,
+ default=None,
+ on_delete=models.SET_DEFAULT,
+ help_text=_(("Web Certificate used by the authentik Core webserver.")),
+ )
+
def __str__(self) -> str:
if self.default:
return "Default tenant"
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 70db3f4f5..232fe6c04 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -15,6 +15,7 @@ import (
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/proxyv2"
"goauthentik.io/internal/web"
+ "goauthentik.io/internal/web/tenant_tls"
)
var running = true
@@ -110,6 +111,12 @@ func attemptProxyStart(ws *web.WebServer, u *url.URL) {
}
continue
}
+ // Init tenant_tls here too since it requires an API Client,
+ // so we just re-use the same one as the outpost uses
+ tw := tenant_tls.NewWatcher(ac.Client)
+ go tw.Start()
+ ws.TenantTLS = tw
+
srv := proxyv2.NewProxyServer(ac, 0)
ws.ProxyServer = srv
ac.Server = srv
diff --git a/internal/outpost/ak/http_tracing.go b/internal/outpost/ak/http_tracing.go
index fb73ec176..c4225eefa 100644
--- a/internal/outpost/ak/http_tracing.go
+++ b/internal/outpost/ak/http_tracing.go
@@ -6,7 +6,6 @@ import (
"net/http"
"github.com/getsentry/sentry-go"
- log "github.com/sirupsen/logrus"
)
type tracingTransport struct {
@@ -26,9 +25,5 @@ func (tt *tracingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
span.SetTag("method", r.Method)
defer span.Finish()
res, err := tt.inner.RoundTrip(r.WithContext(span.Context()))
- log.WithFields(log.Fields{
- "url": r.URL.String(),
- "method": r.Method,
- }).Trace("http request")
return res, err
}
diff --git a/internal/web/tenant_tls/tenant_tls.go b/internal/web/tenant_tls/tenant_tls.go
new file mode 100644
index 000000000..f8048b2b8
--- /dev/null
+++ b/internal/web/tenant_tls/tenant_tls.go
@@ -0,0 +1,81 @@
+package tenant_tls
+
+import (
+ "crypto/tls"
+ "strings"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "goauthentik.io/api"
+ "goauthentik.io/internal/crypto"
+ "goauthentik.io/internal/outpost/ak"
+)
+
+type Watcher struct {
+ client *api.APIClient
+ log *log.Entry
+ cs *ak.CryptoStore
+ fallback *tls.Certificate
+ tenants []api.Tenant
+}
+
+func NewWatcher(client *api.APIClient) *Watcher {
+ cs := ak.NewCryptoStore(client.CryptoApi)
+ l := log.WithField("logger", "authentik.router.tenant_tls")
+ cert, err := crypto.GenerateSelfSignedCert()
+ if err != nil {
+ l.WithError(err).Error("failed to generate default cert")
+ }
+
+ return &Watcher{
+ client: client,
+ log: l,
+ cs: cs,
+ fallback: &cert,
+ }
+}
+
+func (w *Watcher) Start() {
+ ticker := time.NewTicker(time.Minute * 3)
+ w.log.Info("Starting Tenant TLS Checker")
+ for ; true; <-ticker.C {
+ w.Check()
+ }
+}
+
+func (w *Watcher) Check() {
+ tenants, _, err := w.client.CoreApi.CoreTenantsListExecute(api.ApiCoreTenantsListRequest{})
+ if err != nil {
+ w.log.WithError(err).Warning("failed to get tenants")
+ return
+ }
+ for _, t := range tenants.Results {
+ if t.WebCertificate.IsSet() {
+ err := w.cs.AddKeypair(*t.WebCertificate.Get())
+ if err != nil {
+ w.log.WithError(err).Warning("failed to add certificate")
+ }
+ }
+ }
+ w.tenants = tenants.Results
+}
+
+func (w *Watcher) GetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ var bestSelection *api.Tenant
+ for _, t := range w.tenants {
+ if !t.WebCertificate.IsSet() {
+ continue
+ }
+ if *t.Default {
+ bestSelection = &t
+ }
+ if strings.HasSuffix(ch.ServerName, t.Domain) {
+ bestSelection = &t
+ }
+ }
+ if bestSelection == nil {
+ return w.fallback, nil
+ }
+ cert := w.cs.Get(*bestSelection.WebCertificate.Get())
+ return cert, nil
+}
diff --git a/internal/web/tls.go b/internal/web/tls.go
index 71efbacdd..32f07a944 100644
--- a/internal/web/tls.go
+++ b/internal/web/tls.go
@@ -22,6 +22,9 @@ func (ws *WebServer) GetCertificate() func(ch *tls.ClientHelloInfo) (*tls.Certif
return appCert, nil
}
}
+ if ws.TenantTLS != nil {
+ return ws.TenantTLS.GetCertificate(ch)
+ }
ws.log.Trace("using default, self-signed certificate")
return &cert, nil
}
diff --git a/internal/web/web.go b/internal/web/web.go
index 3198247f5..79fd4fdf3 100644
--- a/internal/web/web.go
+++ b/internal/web/web.go
@@ -15,17 +15,17 @@ import (
"goauthentik.io/internal/gounicorn"
"goauthentik.io/internal/outpost/proxyv2"
"goauthentik.io/internal/utils/web"
+ "goauthentik.io/internal/web/tenant_tls"
)
type WebServer struct {
Bind string
BindTLS bool
- LegacyProxy bool
-
stop chan struct{} // channel for waiting shutdown
ProxyServer *proxyv2.ProxyServer
+ TenantTLS *tenant_tls.Watcher
m *mux.Router
lh *mux.Router
@@ -43,8 +43,6 @@ func NewWebServer(g *gounicorn.GoUnicorn) *WebServer {
logginRouter.Use(web.NewLoggingHandler(l, nil))
ws := &WebServer{
- LegacyProxy: true,
-
m: mainHandler,
lh: logginRouter,
log: l,
diff --git a/schema.yml b/schema.yml
index 96960f03d..36fcf8f2d 100644
--- a/schema.yml
+++ b/schema.yml
@@ -2371,6 +2371,11 @@ paths:
schema:
type: string
format: uuid
+ - in: query
+ name: web_certificate
+ schema:
+ type: string
+ format: uuid
tags:
- core
security:
@@ -28211,6 +28216,11 @@ components:
type: string
minLength: 1
description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).'
+ web_certificate:
+ type: string
+ format: uuid
+ nullable: true
+ description: Web Certificate used by the authentik Core webserver.
PatchedTokenRequest:
type: object
description: Token Serializer
@@ -30575,6 +30585,11 @@ components:
event_retention:
type: string
description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).'
+ web_certificate:
+ type: string
+ format: uuid
+ nullable: true
+ description: Web Certificate used by the authentik Core webserver.
required:
- domain
- tenant_uuid
@@ -30618,6 +30633,11 @@ components:
type: string
minLength: 1
description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).'
+ web_certificate:
+ type: string
+ format: uuid
+ nullable: true
+ description: Web Certificate used by the authentik Core webserver.
required:
- domain
Token:
diff --git a/web/src/locales/en.po b/web/src/locales/en.po
index ed4e49fb4..33954dcb6 100644
--- a/web/src/locales/en.po
+++ b/web/src/locales/en.po
@@ -2709,6 +2709,7 @@ msgstr "Loading"
#: src/pages/tenants/TenantForm.ts
#: src/pages/tenants/TenantForm.ts
#: src/pages/tenants/TenantForm.ts
+#: src/pages/tenants/TenantForm.ts
#: src/pages/tokens/TokenForm.ts
#: src/pages/users/UserForm.ts
#: src/pages/users/UserResetEmailForm.ts
@@ -5809,6 +5810,10 @@ msgstr "Warning: You're about to delete the user you're logged in as ({0}). Proc
msgid "Warning: authentik Domain is not configured, authentication will not work."
msgstr "Warning: authentik Domain is not configured, authentication will not work."
+#: src/pages/tenants/TenantForm.ts
+msgid "Web Certificate"
+msgstr "Web Certificate"
+
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
msgid "WebAuthn Authenticators"
msgstr "WebAuthn Authenticators"
diff --git a/web/src/locales/fr_FR.po b/web/src/locales/fr_FR.po
index 263edbf4e..d162856d9 100644
--- a/web/src/locales/fr_FR.po
+++ b/web/src/locales/fr_FR.po
@@ -2688,6 +2688,7 @@ msgstr "Chargement en cours"
#: src/pages/tenants/TenantForm.ts
#: src/pages/tenants/TenantForm.ts
#: src/pages/tenants/TenantForm.ts
+#: src/pages/tenants/TenantForm.ts
#: src/pages/tokens/TokenForm.ts
#: src/pages/users/UserForm.ts
#: src/pages/users/UserResetEmailForm.ts
@@ -5747,6 +5748,10 @@ msgstr ""
msgid "Warning: authentik Domain is not configured, authentication will not work."
msgstr "Avertissement : le domaine d'authentik n'est pas configuré, l'authentification ne fonctionnera pas."
+#: src/pages/tenants/TenantForm.ts
+msgid "Web Certificate"
+msgstr ""
+
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
msgid "WebAuthn Authenticators"
msgstr "Authentificateurs WebAuthn"
diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po
index 9b6ac3cf0..504787bfb 100644
--- a/web/src/locales/pseudo-LOCALE.po
+++ b/web/src/locales/pseudo-LOCALE.po
@@ -2699,6 +2699,7 @@ msgstr ""
#: src/pages/tenants/TenantForm.ts
#: src/pages/tenants/TenantForm.ts
#: src/pages/tenants/TenantForm.ts
+#: src/pages/tenants/TenantForm.ts
#: src/pages/tokens/TokenForm.ts
#: src/pages/users/UserForm.ts
#: src/pages/users/UserResetEmailForm.ts
@@ -5789,6 +5790,10 @@ msgstr ""
msgid "Warning: authentik Domain is not configured, authentication will not work."
msgstr ""
+#: src/pages/tenants/TenantForm.ts
+msgid "Web Certificate"
+msgstr ""
+
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
msgid "WebAuthn Authenticators"
msgstr ""
diff --git a/web/src/pages/tenants/TenantForm.ts b/web/src/pages/tenants/TenantForm.ts
index 45a3541de..95f8bd6c8 100644
--- a/web/src/pages/tenants/TenantForm.ts
+++ b/web/src/pages/tenants/TenantForm.ts
@@ -2,9 +2,16 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
+import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
-import { CoreApi, FlowsApi, FlowsInstancesListDesignationEnum, Tenant } from "@goauthentik/api";
+import {
+ CoreApi,
+ CryptoApi,
+ FlowsApi,
+ FlowsInstancesListDesignationEnum,
+ Tenant,
+} from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../../api/Config";
import "../../elements/forms/FormGroup";
@@ -297,6 +304,35 @@ export class TenantForm extends ModelForm {
${t`Format: "weeks=3;days=2;hours=3,seconds=2".`}
+
+
+
`;