tenants: add web certificate field, make authentik's core certificate configurable based on keypair

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-12-22 11:43:45 +01:00
parent 9e2492be5c
commit 34b11524f1
17 changed files with 358 additions and 12 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Tenant, string> {
${t`Format: "weeks=3;days=2;hours=3,seconds=2".`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Web Certificate`} name="webCertificate">
<select class="pf-c-form-control">
<option
value=""
?selected=${this.instance?.webCertificate === undefined}
>
---------
</option>
${until(
new CryptoApi(DEFAULT_CONFIG)
.cryptoCertificatekeypairsList({
ordering: "name",
hasKey: true,
})
.then((keys) => {
return keys.results.map((key) => {
return html`<option
value=${ifDefined(key.pk)}
?selected=${this.instance?.webCertificate ===
key.pk}
>
${key.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;