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".`}

+ + + `;