Compare commits

..

7 Commits

Author SHA1 Message Date
Marc 'risson' Schmitt
5cb0c4ff3e tls certificates
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-07 15:42:03 +02:00
Marc 'risson' Schmitt
3ffb326d0e wip
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-07 15:42:02 +02:00
Marc 'risson' Schmitt
143758df6e start on application router
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-07 15:42:02 +02:00
Marc 'risson' Schmitt
b30b9a028f continue on handlers
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-07 15:42:01 +02:00
Marc 'risson' Schmitt
6529b4056e wip
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-07 15:42:01 +02:00
Marc 'risson' Schmitt
365192dd57 add container
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-07 15:42:01 +02:00
Marc 'risson' Schmitt
716cef3d62 outpost basics and refresh logic
commit 04669c9f857ecb0b47a5303958bf02de196ba4e9
Merge: 7ff008d6d6 620387f294
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Mon Apr 27 15:36:33 2026 +0200

    Merge branch 'main' into rust-proxy

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 7ff008d6d6
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Fri Apr 24 16:47:38 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 5ad0150fe4
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Fri Apr 24 15:19:32 2026 +0200

    fix page size

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 4f52a79c6a
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Fri Apr 24 14:53:04 2026 +0200

    application refresh

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit a8b8a81375
Merge: 31e7b1dc4b 0459568a96
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Fri Apr 24 13:54:38 2026 +0200

    Merge branch 'main' into rust-proxy

commit 31e7b1dc4b
Merge: 2cb3df2a60 8bf7efecfd
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 23 15:46:53 2026 +0200

    Merge branch 'rust-worker-2' into rust-proxy

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 8bf7efecfd
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 23 15:33:30 2026 +0200

    fix lint

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit b1ceb28f71
Merge: 1fec16b8e0 39e6c41566
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 23 15:26:14 2026 +0200

    Merge branch 'main' into rust-worker-2

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 2cb3df2a60
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 19:00:42 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 5426881797
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 19:00:26 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 3f703bb21b
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 18:23:54 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit b3c0a50f91
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 16:46:54 2026 +0200

    metrics and logging

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 1fec16b8e0
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 13:40:07 2026 +0200

    run -> start

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 8657d74dc9
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 2 13:22:10 2026 +0200

    root: init rust worker

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 347df15f50
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 14:00:28 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit cf2ed15ced
Merge: dc1d99288f b220e80a0d
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 13:42:43 2026 +0200

    Merge branch 'rust-worker-2' into rust-proxy

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit b220e80a0d
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 16 13:40:07 2026 +0200

    run -> start

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 54f6b5c73c
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 2 13:22:10 2026 +0200

    root: init rust worker

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 9fad68bdad
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:12:01 2026 +0200

    packages/ak-common/tracing: get sentry config from API for outposts

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit dc1d99288f
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:51:28 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 8fb795ec89
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:41:40 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit f8f84f5f0b
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:41:33 2026 +0200

    fixup

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 5812558463
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:38:06 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 513462f78d
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:38:02 2026 +0200

    fixup

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 833912b712
Merge: 9fba928666 78a4b06ab3
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:32:31 2026 +0200

    Merge branch 'rust-worker-2' into rust-proxy

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 78a4b06ab3
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 2 13:22:10 2026 +0200

    root: init rust worker

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit c38e3cbbcf
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:12:01 2026 +0200

    packages/ak-common/tracing: get sentry config from API for outposts

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 9fba928666
Merge: ce8f33416e 668f37ea41
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 17:16:50 2026 +0200

    Merge branch 'main' into rust-proxy

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit ce8f33416e
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Wed Apr 15 16:41:26 2026 +0200

    ws

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 6308ec3360
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Tue Apr 14 15:04:03 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit 915bf6942e
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Fri Apr 10 17:16:32 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit e63d2afb29
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Fri Apr 10 14:10:05 2026 +0200

    wip

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

commit d103cea26a
Author: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Date:   Thu Apr 2 13:22:10 2026 +0200

    root: init rust worker

    Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-07 15:41:57 +02:00
26 changed files with 1213 additions and 137 deletions

184
Cargo.lock generated
View File

@@ -143,6 +143,45 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "asn1-rs"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
dependencies = [
"asn1-rs-derive",
"asn1-rs-impl",
"displaydoc",
"nom",
"num-traits",
"rusticata-macros",
"thiserror 2.0.18",
"time",
]
[[package]]
name = "asn1-rs-derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "asn1-rs-impl"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -176,10 +215,13 @@ dependencies = [
"arc-swap",
"argh",
"authentik-axum",
"authentik-client",
"authentik-common",
"axum",
"axum-server",
"color-eyre",
"eyre",
"futures",
"hyper-unix-socket",
"hyper-util",
"metrics",
@@ -187,9 +229,19 @@ dependencies = [
"nix 0.31.2",
"pyo3",
"pyo3-build-config",
"rand 0.10.1",
"rustls",
"serde",
"serde_json",
"serde_repr",
"sqlx",
"time",
"tokio",
"tokio-retry2",
"tokio-tungstenite",
"tower",
"tracing",
"url",
"uuid",
"which",
]
@@ -247,6 +299,7 @@ dependencies = [
"nix 0.31.2",
"notify",
"pin-project-lite",
"rcgen",
"reqwest",
"reqwest-middleware",
"rustls",
@@ -542,6 +595,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
name = "chrono"
version = "0.4.44"
@@ -779,6 +843,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.4.0"
@@ -873,6 +946,20 @@ dependencies = [
"zeroize",
]
[[package]]
name = "der-parser"
version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
dependencies = [
"asn1-rs",
"displaydoc",
"nom",
"num-bigint",
"num-traits",
"rusticata-macros",
]
[[package]]
name = "deranged"
version = "0.5.8"
@@ -1291,6 +1378,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasip2",
"wasip3",
]
@@ -2182,6 +2270,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
@@ -2411,6 +2509,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "oid-registry"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
dependencies = [
"asn1-rs",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -2812,6 +2919,17 @@ dependencies = [
"rand_core 0.9.5",
]
[[package]]
name = "rand"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20",
"getrandom 0.4.2",
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
@@ -2850,6 +2968,12 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_xoshiro"
version = "0.7.0"
@@ -2877,6 +3001,19 @@ dependencies = [
"bitflags 2.11.0",
]
[[package]]
name = "rcgen"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e"
dependencies = [
"aws-lc-rs",
"rustls-pki-types",
"time",
"x509-parser",
"yasna",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -3040,6 +3177,15 @@ dependencies = [
"semver",
]
[[package]]
name = "rusticata-macros"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
dependencies = [
"nom",
]
[[package]]
name = "rustix"
version = "1.1.4"
@@ -3419,7 +3565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -3430,7 +3576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -4000,8 +4146,12 @@ checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
dependencies = [
"futures-util",
"log",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tungstenite",
"webpki-roots 0.26.11",
]
[[package]]
@@ -4215,8 +4365,11 @@ dependencies = [
"httparse",
"log",
"rand 0.9.4",
"rustls",
"rustls-pki-types",
"sha1",
"thiserror 2.0.18",
"url",
]
[[package]]
@@ -5087,6 +5240,24 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "x509-parser"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
dependencies = [
"asn1-rs",
"aws-lc-rs",
"data-encoding",
"der-parser",
"lazy_static",
"nom",
"oid-registry",
"rusticata-macros",
"thiserror 2.0.18",
"time",
]
[[package]]
name = "yaml-rust2"
version = "0.10.4"
@@ -5098,6 +5269,15 @@ dependencies = [
"hashlink",
]
[[package]]
name = "yasna"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
dependencies = [
"time",
]
[[package]]
name = "yoke"
version = "0.8.1"

View File

@@ -50,6 +50,11 @@ notify = "= 8.2.0"
pin-project-lite = "= 0.2.17"
pyo3 = "= 0.28.3"
pyo3-build-config = "= 0.28.3"
rand = "= 0.10.1"
rcgen = { version = "= 0.14.7", default-features = false, features = [
"aws_lc_rs",
"fips",
] }
regex = "= 1.12.3"
reqwest = { version = "= 0.13.3", features = [
"form",
@@ -100,6 +105,10 @@ time = { version = "= 0.3.47", features = ["macros"] }
tokio = { version = "= 1.52.1", features = ["full", "tracing"] }
tokio-retry2 = "= 0.9.1"
tokio-rustls = "= 0.26.4"
tokio-tungstenite = { version = "= 0.29.0", features = [
"rustls-tls-webpki-roots",
"url",
] }
tokio-util = { version = "= 0.7.18", features = ["full"] }
tower = "= 0.5.3"
tower-http = { version = "= 0.6.8", features = ["timeout"] }
@@ -260,28 +269,41 @@ publish.workspace = true
[features]
default = ["core", "proxy"]
core = ["ak-common/core", "dep:pyo3", "dep:sqlx"]
proxy = ["ak-common/proxy"]
proxy = ["ak-common/proxy", "dep:ak-client"]
[build-dependencies]
pyo3-build-config.workspace = true
[dependencies]
ak-axum.workspace = true
ak-client = { workspace = true, optional = true }
ak-common.workspace = true
arc-swap.workspace = true
argh.workspace = true
axum-server.workspace = true
axum.workspace = true
color-eyre.workspace = true
eyre.workspace = true
futures.workspace = true
hyper-unix-socket.workspace = true
hyper-util.workspace = true
metrics.workspace = true
metrics-exporter-prometheus.workspace = true
metrics.workspace = true
nix.workspace = true
pyo3 = { workspace = true, optional = true }
rand.workspace = true
rustls.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_repr.workspace = true
sqlx = { workspace = true, optional = true }
time.workspace = true
tokio-retry2.workspace = true
tokio-tungstenite.workspace = true
tokio.workspace = true
tower.workspace = true
tracing.workspace = true
url.workspace = true
uuid.workspace = true
which.workspace = true

View File

@@ -101,8 +101,6 @@ RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
rustc --version && \
cargo --version
RUN cat /root/.rustup/settings.toml
# Stage: Download uv
FROM ghcr.io/astral-sh/uv:0.11.5@sha256:555ac94f9a22e656fc5f2ce5dfee13b04e94d099e46bb8dd3a73ec7263f2e484 AS uv
# Stage: Base python image

View File

@@ -21,33 +21,45 @@ COPY web .
RUN npm run build-proxy
# Stage 2: Build
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.26.2-trixie@sha256:4a7137ea573f79c86ae451ff05817ed762ef5597fcf732259e97abeb3108d873 AS builder
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:7726387c78b5787d2146868c2ccc8948a3591d0a5a6436f7780c8c28acc76341 AS builder
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
ARG GOOS=$TARGETOS
ARG GOARCH=$TARGETARCH
WORKDIR /go/src/goauthentik.io
ENV PATH="/root/.cargo/bin:$PATH"
SHELL ["/bin/sh", "-o", "pipefail", "-c"]
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
dpkg --add-architecture arm64 && \
--mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
apt-get update && \
apt-get install -y --no-install-recommends crossbuild-essential-arm64 gcc-aarch64-linux-gnu
# Required for installing pip packages
apt-get install -y --no-install-recommends \
# Build essentials
build-essential \
# aws-lc deps
cmake clang golang && \
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain none && \
rustup install && \
rustup default "$(sed -n 's/channel = "\(.*\)"/\1/p' rust-toolchain.toml)" && \
rustc --version && \
cargo --version
# See https://github.com/aws/aws-lc-rs/issues/569
ENV AWS_LC_FIPS_SYS_CC=clang
RUN --mount=type=bind,target=/go/src/goauthentik.io/go.mod,src=./go.mod \
--mount=type=bind,target=/go/src/goauthentik.io/go.sum,src=./go.sum \
--mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
go build -o /go/proxy ./cmd/proxy
RUN --mount=type=bind,target=rust-toolchain.toml,src=rust-toolchain.toml \
--mount=type=bind,target=Cargo.toml,src=Cargo.toml \
--mount=type=bind,target=Cargo.lock,src=Cargo.lock \
--mount=type=bind,target=.cargo/,src=.cargo/ \
--mount=type=bind,target=src/,src=src/ \
--mount=type=bind,target=packages/,src=packages/ \
--mount=type=bind,target=authentik/lib/default.yml,src=authentik/lib/default.yml \
# Required otherwise workspace discovery fails
--mount=type=bind,target=website/scripts/docsmg/,src=website/scripts/docsmg/ \
--mount=type=cache,id=cargo-git-db-$TARGETARCH$TARGETVARIANT,target=/root/.cargo/git/db/ \
--mount=type=cache,id=cargo-registry-$TARGETARCH$TARGETVARIANT,target=/root/.cargo/registry/ \
--mount=type=cache,id=rust-target-$TARGETARCH$TARGETVARIANT,target=/build/target/ \
cargo build --package authentik --no-default-features --features proxy --locked --release && \
cp ./target/release/authentik /bin/authentik
# Stage 3: Run
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:7726387c78b5787d2146868c2ccc8948a3591d0a5a6436f7780c8c28acc76341
@@ -72,13 +84,13 @@ RUN apt-get update && \
apt-get clean && \
rm -rf /tmp/* /var/lib/apt/lists/*
COPY --from=builder /go/proxy /
COPY --from=builder /bin/authentik /
COPY --from=web-builder /static/robots.txt /web/robots.txt
COPY --from=web-builder /static/security.txt /web/security.txt
COPY --from=web-builder /static/dist/ /web/dist/
COPY --from=web-builder /static/authentik/ /web/authentik/
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/proxy", "healthcheck" ]
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/authentik", "healthcheck" ]
EXPOSE 9000 9300 9443
@@ -87,4 +99,4 @@ USER 1000
ENV TMPDIR=/dev/shm/ \
GOFIPS=1
ENTRYPOINT ["/proxy"]
ENTRYPOINT ["/authentik", "proxy"]

View File

@@ -27,6 +27,7 @@ ipnet.workspace = true
json-subscriber.workspace = true
notify.workspace = true
pin-project-lite.workspace = true
rcgen.workspace = true
reqwest.workspace = true
reqwest-middleware.workspace = true
rustls.workspace = true

View File

@@ -1,6 +1,6 @@
//! Utilities for working with the authentik API client.
use ak_client::apis::configuration::Configuration;
use ak_client::{apis::configuration::Configuration, models::Pagination};
use eyre::{Result, eyre};
use url::Url;
@@ -60,6 +60,42 @@ pub fn make_config() -> Result<Configuration> {
})
}
/// Fetch all pages from a paginated API endpoint, returning all results combined.
///
/// - `fetch`: a function that takes a page number and returns a future resolving to a paginated
/// response.
/// - `get_pagination`: a function that extracts the [`Pagination`] metadata from a response.
/// - `get_results`: a function that extracts the result items from a response.
pub async fn fetch_all<T, R, E, F, Fut>(
fetch: F,
get_pagination: impl Fn(&R) -> &Pagination,
get_results: impl Fn(R) -> Vec<T>,
) -> std::result::Result<Vec<T>, E>
where
F: Fn(i32) -> Fut,
Fut: Future<Output = std::result::Result<R, E>>,
{
let mut page = 1;
let mut results = Vec::with_capacity(0);
loop {
let response = fetch(page).await?;
let next = get_pagination(&response).next;
if page == 1 {
let count = get_pagination(&response).count as usize;
results.reserve(count);
}
results.extend(get_results(response));
if next > 0.0 {
page += 1;
} else {
break;
}
}
Ok(results)
}
#[cfg(test)]
mod tests {
use serde_json::json;

View File

@@ -3,8 +3,9 @@ use std::{collections::HashMap, net::SocketAddr, num::NonZeroUsize};
use ipnet::IpNet;
use serde::{Deserialize, Serialize};
pub(super) const KEYS_TO_PARSE_AS_LIST: [&str; 4] = [
pub(super) const KEYS_TO_PARSE_AS_LIST: [&str; 5] = [
"listen.http",
"listen.https",
"listen.metrics",
"listen.trusted_proxy_cidrs",
"log.http_headers",
@@ -59,6 +60,7 @@ pub struct PostgreSQLConfig {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListenConfig {
pub http: Vec<SocketAddr>,
pub https: Vec<SocketAddr>,
pub metrics: Vec<SocketAddr>,
pub debug_tokio: SocketAddr,
pub trusted_proxy_cidrs: Vec<IpNet>,

View File

@@ -7,6 +7,8 @@ use tracing::trace;
use crate::config;
pub mod self_signed;
/// Dummy resolver for FIPS compliance check.
#[derive(Debug)]
struct EmptyCertResolver;

View File

@@ -0,0 +1,52 @@
use eyre::Result;
use rcgen::{
Certificate, CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, KeyPair,
KeyUsagePurpose, PKCS_RSA_SHA256, SanType,
};
use rustls::{
crypto::aws_lc_rs::sign::any_supported_type,
pki_types::{CertificateDer, PrivateKeyDer},
sign::CertifiedKey,
};
use time::{Duration, OffsetDateTime};
pub fn generate() -> Result<(Certificate, KeyPair)> {
let signing_key = KeyPair::generate_for(&PKCS_RSA_SHA256)?;
let mut params = CertificateParams::default();
params.not_before = OffsetDateTime::now_utc();
params.not_after = OffsetDateTime::now_utc() + Duration::days(365);
params.distinguished_name = {
let mut dn = DistinguishedName::new();
dn.push(DnType::OrganizationName, "authentik");
dn.push(DnType::CommonName, "authentik default certificate");
dn
};
params.subject_alt_names = vec![SanType::DnsName("*".try_into()?)];
params.key_usages = vec![
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::KeyEncipherment,
];
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
let cert = params.self_signed(&signing_key)?;
Ok((cert, signing_key))
}
pub fn generate_certifiedkey() -> Result<CertifiedKey> {
let (cert, keypair) = generate()?;
let cert_der = cert.der().to_vec();
let key_der = keypair.serialize_der();
let private_key =
PrivateKeyDer::try_from(key_der).map_err(|_| rcgen::Error::CouldNotParseKeyPair)?;
let signing_key =
any_supported_type(&private_key).map_err(|_| rcgen::Error::CouldNotParseKeyPair)?;
Ok(CertifiedKey::new(
vec![CertificateDer::from(cert_der)],
signing_key,
))
}

View File

@@ -30,12 +30,12 @@ pub fn install() -> Result<()> {
}
if config.debug {
let console_layer = console_subscriber::ConsoleLayer::builder()
.server_addr(config.listen.debug_tokio)
.spawn();
// let console_layer = console_subscriber::ConsoleLayer::builder()
// .server_addr(config.listen.debug_tokio)
// .spawn();
tracing_subscriber::registry()
.with(ErrorLayer::default())
.with(console_layer)
// .with(console_layer)
.with(
fmt::layer()
.compact()
@@ -186,12 +186,9 @@ pub mod sentry {
sentry_dsn: Some(config.sentry_dsn),
environment: config.environment,
send_pii: config.send_pii,
#[expect(
clippy::cast_possible_truncation,
reason = "This is fine, we'll never get big values here."
)]
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
reason = "This is fine, we'll never get big values here."
)]
sample_rate: config.traces_sample_rate as f32,

View File

@@ -8,6 +8,8 @@ use eyre::{Result, eyre};
use tracing::{error, info, trace};
mod metrics;
#[cfg(feature = "proxy")]
mod outpost;
#[cfg(feature = "core")]
mod server;
#[cfg(feature = "core")]
@@ -29,6 +31,8 @@ enum Command {
Server(server::Cli),
#[cfg(feature = "core")]
Worker(worker::Cli),
#[cfg(feature = "proxy")]
Proxy(outpost::proxy::Cli),
}
#[derive(Debug, FromArgs, PartialEq)]
@@ -53,6 +57,8 @@ fn main() -> Result<()> {
Command::Server(_) => Mode::set(Mode::Server)?,
#[cfg(feature = "core")]
Command::Worker(_) => Mode::set(Mode::Worker)?,
#[cfg(feature = "proxy")]
Command::Proxy(_) => Mode::set(Mode::Proxy)?,
}
trace!("installing error formatting");
@@ -108,6 +114,10 @@ fn main() -> Result<()> {
let workers = worker::start(args, &mut tasks)?;
metrics.workers.store(Some(workers));
}
#[cfg(feature = "proxy")]
Command::Proxy(args) => {
outpost::start::<outpost::proxy::ProxyOutpost>(args, &mut tasks).await?;
}
}
let errors = tasks.run().await;

312
src/outpost/event.rs Normal file
View File

@@ -0,0 +1,312 @@
use std::{fmt::Display, sync::Arc};
use ak_common::{Arbiter, Tasks, VERSION, api, arbiter, authentik_build_hash};
use axum::http::{HeaderValue, header::AUTHORIZATION};
use eyre::{Result, eyre};
use futures::{SinkExt as _, StreamExt as _};
use nix::unistd::gethostname;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use time::UtcDateTime;
use tokio::{
signal::unix::SignalKind,
time::{Duration, interval, sleep},
};
use tokio_tungstenite::tungstenite::{Message, client::IntoClientRequest as _};
use tracing::{debug, info, instrument, trace, warn};
use url::Url;
use crate::outpost::{Outpost, OutpostController};
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Clone, Copy, Eq)]
#[repr(u8)]
enum EventKind {
/// Code used to acknowledge a previous message.
Ack = 0,
/// Code used to send a healthcheck keepalive.
Hello = 1,
/// Code received to trigger a config update.
TriggerUpdate = 2,
/// Code received to trigger some provider specific function.
ProviderSpecific = 3,
/// Code received to identify the end of a session.
SessionEnd = 4,
}
impl Display for EventKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ack => write!(f, "Ack"),
Self::Hello => write!(f, "Hello"),
Self::TriggerUpdate => write!(f, "TriggerUpdate"),
Self::ProviderSpecific => write!(f, "ProviderSpecific"),
Self::SessionEnd => write!(f, "SessionEnd"),
}
}
}
#[derive(Serialize, Deserialize)]
struct Event {
instruction: EventKind,
args: serde_json::Value,
}
#[derive(Debug, Deserialize)]
pub(crate) struct EventSessionEnd {
session_id: String,
}
fn build_ws_url(mut url: Url, outpost_pk: &str, instance_uuid: &str, attempt: u32) -> Result<Url> {
let ws_scheme = match url.scheme() {
"https" => "wss",
"http" => "ws",
other => return Err(eyre!("Unsupported scheme for WebSocket URL: {other}")),
};
url.set_scheme(ws_scheme)
.map_err(|()| eyre!("Failed to set URL scheme to {ws_scheme}"))?;
url.set_path(&format!("{}ws/outpost/{outpost_pk}/", url.path()));
url.query_pairs_mut()
.append_pair("instance_uuid", instance_uuid)
.append_pair("attempt", &attempt.to_string());
Ok(url)
}
fn hello_args(instance_uuid: &str) -> serde_json::Value {
let raw_hostname = gethostname().unwrap_or_default();
let hostname = raw_hostname.to_string_lossy();
serde_json::json!({
"version": VERSION,
"buildHash": authentik_build_hash(None),
"uuid": instance_uuid,
// TODO: rust version and AWS-LC versions
"hostname": hostname,
})
}
#[instrument(skip_all)]
async fn handle_event<O: Outpost>(
controller: Arc<OutpostController>,
outpost: Arc<O>,
event: Event,
) -> Result<()> {
match event.instruction {
EventKind::Ack | EventKind::Hello => {}
EventKind::TriggerUpdate => {
info!("received update trigger, refreshing outpost");
sleep(controller.reload_offset).await;
controller.refresh().await?;
debug!("outpost controller has been refreshed");
outpost.refresh().await?;
debug!("outpost has been refreshed");
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "This is fine, we'll never get big values here."
)]
controller
.m_last_update
.set(UtcDateTime::now().unix_timestamp() as f64);
}
EventKind::SessionEnd => {
let event: EventSessionEnd = serde_json::from_value(event.args)?;
outpost.end_session(event).await?;
}
#[expect(
clippy::unimplemented,
reason = "this is only relevant for the RAC provider"
)]
EventKind::ProviderSpecific => unimplemented!(),
}
Ok(())
}
async fn watch_events_inner<O: Outpost>(
arbiter: Arbiter,
controller: Arc<OutpostController>,
outpost: Arc<O>,
attempt: u32,
) -> Result<()> {
let server_config = api::ServerConfig::new()?;
let ws_url = build_ws_url(
server_config.host,
&controller.outpost.load().pk.to_string(),
&controller.instance_uuid.to_string(),
attempt,
)?;
debug!(url = %ws_url, "connecting to websocket");
let mut request = ws_url.into_client_request()?;
let token = controller
.api_config
.bearer_access_token
.as_deref()
.unwrap_or("");
request.headers_mut().insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {token}"))?,
);
let (ws_stream, _response) = tokio_tungstenite::connect_async(request).await?;
let (mut ws_write, mut ws_read) = ws_stream.split();
info!(
outpost = %controller.outpost.load().pk,
"connected to websocket"
);
controller.m_connection.set(1_u8);
let get_refresh_interval = || {
let mut interval = controller.outpost.load().refresh_interval_s;
// Ensure timer interval is not negative or 0.
// If it is, we default to 5 minutes.
if interval <= 0_i32 {
interval = 60_i32 * 5_i32;
}
// Clamp interval to be at least 30 seconds.
if interval < 30_i32 {
interval = 30_i32;
}
// infallible because we bound it to be positive above
Duration::from_secs(interval.try_into().expect("infallible"))
};
let mut refresh_interval = interval(get_refresh_interval());
let mut heartbeat_interval = interval(Duration::from_secs(10));
let mut events_rx = arbiter.events_subscribe();
loop {
tokio::select! {
_ = refresh_interval.tick() => {
info!("refreshing outpost on interval");
if let Err(err) = handle_event(
Arc::clone(&controller),
Arc::clone(&outpost),
Event {
instruction: EventKind::TriggerUpdate,
args: serde_json::Value::Null
}
).await {
warn!(?err, "failed to refresh");
}
refresh_interval = interval(get_refresh_interval());
// Since we re-create the interval, we need to make it tick instantly to avoid
// ending up in a never-ending tick-loop.
refresh_interval.tick().await;
},
_ = heartbeat_interval.tick() => {
let ping = Event {
instruction: EventKind::Hello,
args: hello_args(&controller.instance_uuid.to_string()),
};
ws_write.send(Message::text(serde_json::to_string(&ping)?)).await?;
trace!("sent websocket hello (heartbeat)");
},
Ok(arbiter::Event::Signal(signal)) = events_rx.recv() => {
if signal == SignalKind::user_defined1() {
info!("refreshing outpost on signal");
if let Err(err) = handle_event(
Arc::clone(&controller),
Arc::clone(&outpost),
Event {
instruction: EventKind::TriggerUpdate,
args: serde_json::Value::Null
}
).await {
warn!(?err, "failed to refresh");
}
}
},
msg = ws_read.next() => {
let Some(msg) = msg else {
break;
};
let msg = msg?;
match msg {
Message::Text(text) => {
let Ok(event): Result<Event, _> = serde_json::from_str(&text) else {
warn!(data = text.as_str(), "failed to parse event");
continue;
};
trace!(event = %event.instruction, "received websocket event");
if let Err(err) = handle_event(
Arc::clone(&controller),
Arc::clone(&outpost),
event,
).await {
warn!(?err, "failed to handle event");
}
},
Message::Ping(data) => {
ws_write.send(Message::Pong(data)).await?;
},
Message::Close(_) => {
break;
},
_ => {},
}
},
() = arbiter.shutdown() => break,
}
}
Ok(())
}
async fn watch_events<O: Outpost>(
arbiter: Arbiter,
controller: Arc<OutpostController>,
outpost: Arc<O>,
) -> Result<()> {
const MAX_BACKOFF: Duration = Duration::from_mins(5);
let mut backoff = Duration::from_secs(1);
let mut attempt: u32 = 0;
loop {
tokio::select! {
() = arbiter.shutdown() => break,
res = watch_events_inner(
arbiter.clone(),
Arc::clone(&controller),
Arc::clone(&outpost),
attempt
) => {
controller.m_connection.set(0_u8);
match res {
Ok(()) => debug!("websocket disconnected cleanly"),
Err(err) => warn!(?err, attempt, "websocket error"),
}
info!(attempt, delay = backoff.as_secs(), "reconnecting websocket in {}s...", backoff.as_secs());
tokio::select! {
() = arbiter.shutdown() => break,
() = sleep(backoff) => {}
}
backoff = (backoff * 2).min(MAX_BACKOFF);
attempt += 1;
}
}
}
info!("stopping event watcher");
Ok(())
}
pub(crate) fn start<O: Outpost + 'static>(
tasks: &mut Tasks,
controller: Arc<OutpostController>,
outpost: Arc<O>,
) -> Result<()> {
let arbiter = tasks.arbiter();
tasks
.build_task()
.name(&format!("{}::watch_events", module_path!()))
.spawn(watch_events(arbiter, controller, outpost))?;
Ok(())
}

123
src/outpost/mod.rs Normal file
View File

@@ -0,0 +1,123 @@
use std::{sync::Arc, time::Duration};
use ak_client::{
apis::{configuration::Configuration, outposts_api::outposts_instances_list},
models::Outpost as OutpostModel,
};
use ak_common::{Tasks, VERSION, api, authentik_build_hash};
use arc_swap::ArcSwap;
use eyre::{Result, eyre};
use tracing::{debug, info, instrument};
use uuid::Uuid;
pub(crate) mod event;
#[cfg(feature = "proxy")]
pub(crate) mod proxy;
pub(crate) trait Outpost: Send + Sync + Sized {
const OUTPOST_TYPE: &'static str;
type Cli: Send + Sync;
async fn new(controller: Arc<OutpostController>) -> Result<Self>;
fn start(self: Arc<Self>, tasks: &mut Tasks) -> Result<()>;
fn refresh(&self) -> impl Future<Output = Result<()>> + Send;
fn end_session(&self, event: event::EventSessionEnd)
-> impl Future<Output = Result<()>> + Send;
}
#[derive(Debug)]
pub(crate) struct OutpostController {
api_config: Configuration,
outpost: ArcSwap<OutpostModel>,
instance_uuid: Uuid,
reload_offset: Duration,
m_info: metrics::Gauge,
m_last_update: metrics::Gauge,
m_connection: metrics::Gauge,
}
impl OutpostController {
#[instrument(skip_all)]
async fn get_outpost(api_config: &Configuration) -> Result<OutpostModel> {
let outposts = outposts_instances_list(
api_config, None, None, None, None, None, None, None, None, None, None, None, None,
)
.await?;
let Some(outpost) = outposts.results.into_iter().next() else {
return Err(eyre!(
"No outposts found with given token, ensure the given token corresponds to an \
authentik Outpost"
));
};
debug!(name = outpost.name, "fetched outpost configuration");
Ok(outpost)
}
#[instrument(skip_all)]
async fn new<O: Outpost>() -> Result<Self> {
let api_config = api::make_config()?;
let outpost = Self::get_outpost(&api_config).await?;
let instance_uuid = Uuid::new_v4();
let m_labels = [
("outpost_name", outpost.name.clone()),
("outpost_type", O::OUTPOST_TYPE.to_owned()),
("uuid", instance_uuid.to_string()),
("version", VERSION.to_owned()),
("build", authentik_build_hash(None)),
];
metrics::describe_gauge!("authentik_outpost_info", "Outpost info");
let m_info = metrics::gauge!("authentik_outpost_info", &m_labels);
metrics::describe_gauge!("authentik_outpost_last_update", "Time of last update");
let m_last_update = metrics::gauge!("authentik_outpost_last_update", &m_labels);
metrics::describe_gauge!("authentik_outpost_connection", "Connection status");
let m_connection = metrics::gauge!("authentik_outpost_connection", &m_labels);
let reload_offset = Duration::from_secs(rand::random_range(0..10));
let controller = Self {
api_config,
outpost: ArcSwap::from_pointee(outpost),
instance_uuid,
reload_offset,
m_info,
m_last_update,
m_connection,
};
info!(embedded = controller.is_embedded(), "outpost mode");
debug!(?reload_offset, "HA Reload offset");
Ok(controller)
}
fn is_embedded(&self) -> bool {
self.outpost
.load()
.managed
.as_ref()
.and_then(|m| m.as_deref())
.is_some_and(|m| m == "goauthentik.io/outposts/embedded")
}
async fn refresh(&self) -> Result<()> {
let outpost = Self::get_outpost(&self.api_config).await?;
self.outpost.swap(Arc::new(outpost));
Ok(())
}
}
#[instrument(skip_all)]
pub(crate) async fn start<O: Outpost + 'static>(_cli: O::Cli, tasks: &mut Tasks) -> Result<()> {
let controller = Arc::new(OutpostController::new::<O>().await?);
let outpost = Arc::new(O::new(Arc::clone(&controller)).await?);
event::start(tasks, Arc::clone(&controller), Arc::clone(&outpost))?;
outpost.start(tasks)?;
controller.m_info.set(1_u8);
Ok(())
}

View File

@@ -0,0 +1,82 @@
use std::sync::Arc;
use ak_client::{
apis::crypto_api::{
crypto_certificatekeypairs_view_certificate_retrieve,
crypto_certificatekeypairs_view_private_key_retrieve,
},
models::ProxyOutpostConfig,
};
use axum::Router;
use eyre::{Result, eyre};
use rustls::{
crypto::CryptoProvider,
pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject as _},
sign::CertifiedKey,
};
use tracing::instrument;
use url::Url;
use crate::outpost::proxy::ProxyOutpost;
const REDIRECT_PARAM: &str = "rd";
const CALLBACK_SIGNATURE: &str = "X-authentik-auth-callback";
const LOGOUT_SIGNATURE: &str = "X-authentik-logout";
#[derive(Debug)]
pub(super) struct Application {
pub(super) host: String,
pub(super) provider: ProxyOutpostConfig,
pub(super) router: Router,
pub(super) cert: Option<Arc<CertifiedKey>>,
}
impl Application {
#[instrument(skip_all)]
pub(super) async fn new(outpost: &ProxyOutpost, provider: ProxyOutpostConfig) -> Result<Self> {
let external_url = Url::parse(&provider.external_host)?;
if !external_url.has_authority() {
return Err(eyre!("no host in external host"));
}
let external_host = external_url.authority();
// TODO: extract this to a certificate store to avoid re-fetching the certificate every time
let cert = if let Some(Some(kp_uuid)) = provider.certificate {
let cert = crypto_certificatekeypairs_view_certificate_retrieve(
&outpost.controller.api_config,
&kp_uuid.to_string(),
None,
)
.await?;
let key = crypto_certificatekeypairs_view_private_key_retrieve(
&outpost.controller.api_config,
&kp_uuid.to_string(),
None,
)
.await?;
let cert_chain = CertificateDer::pem_reader_iter(cert.data.as_bytes())
.collect::<Result<Vec<_>, _>>()?;
let key_der = PrivateKeyDer::from_pem_reader(key.data.as_bytes())?;
let provider = CryptoProvider::get_default().expect("no rustls provider installed");
Some(Arc::new(CertifiedKey::new(
cert_chain,
provider.key_provider.load_private_key(key_der)?,
)))
} else {
None
};
let _redirect_url = {
let mut redirect_url = external_url.join("outpost.goauthentik.io/callback")?;
redirect_url.set_query(Some(&format!("{CALLBACK_SIGNATURE}=true")));
redirect_url
};
Ok(Self {
host: external_host.to_owned(),
provider,
router: Router::new(),
cert,
})
}
}

View File

@@ -0,0 +1,76 @@
use std::sync::Arc;
use ak_axum::{error::Result, extract::host::Host};
use axum::{
extract::{Request, State},
http::{Method, StatusCode, header::CONTENT_TYPE},
response::{IntoResponse as _, Response},
};
use metrics::histogram;
use serde_json::json;
use tokio::time::Instant;
use tower::util::ServiceExt as _;
use tracing::{Instrument as _, field, info_span, instrument, trace, warn};
use crate::outpost::proxy::ProxyOutpost;
#[instrument(skip_all)]
pub(super) async fn handle_ping(
method: Method,
Host(host): Host,
State(outpost): State<Arc<ProxyOutpost>>,
) -> Response {
let start = Instant::now();
histogram!(
"authentik_outpost_proxy_request_duration_seconds",
"outpost_name" => outpost.controller.outpost.load().name.clone(),
"method" => method.to_string(),
"host" => host,
"type" => "ping",
)
.record(start.elapsed().as_secs_f64());
StatusCode::NO_CONTENT.into_response()
}
#[instrument(skip_all)]
pub(super) async fn default(
method: Method,
Host(host): Host,
State(outpost): State<Arc<ProxyOutpost>>,
request: Request,
) -> Result<Response> {
let span = info_span!("proxy outpost request", user = field::Empty);
let start = Instant::now();
let Some(app) = outpost.lookup_app(&host) else {
trace!(headers = ?request.headers(), "tracing headers for no hostname match");
warn!("no app for hostname");
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.header(CONTENT_TYPE, "application/json")
.body(
json!({
"message": "no app for hostname",
"host": host,
"detail": format!("check the outpost settings and make sure '{host}' is included."),
})
.to_string()
.into(),
)
.expect("infallible"));
};
trace!("passing to application");
let response = app.router.clone().oneshot(request).instrument(span).await?;
histogram!(
"authentik_outpost_proxy_request_duration_seconds",
"outpost_name" => outpost.controller.outpost.load().name.clone(),
"method" => method.to_string(),
"host" => host,
"type" => "app",
)
.record(start.elapsed().as_secs_f64());
Ok(response)
}

228
src/outpost/proxy/mod.rs Normal file
View File

@@ -0,0 +1,228 @@
use std::{collections::HashMap, sync::Arc};
use ak_axum::router::wrap_router;
use ak_client::{apis::outposts_api::outposts_proxy_list, models::ProxyMode};
use ak_common::{Tasks, api::fetch_all, config, tls};
use arc_swap::ArcSwap;
use argh::FromArgs;
use axum::Router;
use axum_server::tls_rustls::RustlsConfig;
use eyre::Result;
use rustls::{
ServerConfig,
server::{ClientHello, ResolvesServerCert},
sign::CertifiedKey,
};
use tracing::{debug, error, info, instrument, warn};
use crate::outpost::{Outpost, OutpostController, proxy::application::Application};
mod application;
mod handlers;
#[derive(Debug, Default, FromArgs, PartialEq, Eq)]
/// Run the authentik proxy outpost.
#[argh(subcommand, name = "proxy")]
#[expect(
clippy::empty_structs_with_brackets,
reason = "argh doesn't support unit structs"
)]
pub(crate) struct Cli {}
#[derive(Debug)]
pub(crate) struct ProxyOutpost {
controller: Arc<OutpostController>,
apps: ArcSwap<HashMap<String, Arc<Application>>>,
default_cert: Arc<CertifiedKey>,
}
impl Outpost for ProxyOutpost {
type Cli = Cli;
const OUTPOST_TYPE: &'static str = "proxy";
#[instrument(skip_all)]
async fn new(controller: Arc<OutpostController>) -> Result<Self> {
Ok(Self {
controller,
apps: ArcSwap::from_pointee(HashMap::with_capacity(0)),
default_cert: Arc::new(tls::self_signed::generate_certifiedkey()?),
})
}
fn start(self: Arc<Self>, tasks: &mut Tasks) -> Result<()> {
let router = build_router(Arc::clone(&self));
for addr in config::get().listen.http.iter().copied() {
ak_axum::server::start_plain(tasks, "proxy-outpost", router.clone(), addr)?;
}
for addr in config::get().listen.https.iter().copied() {
let resolver = Arc::clone(&self);
let server_config = ServerConfig::builder()
.with_no_client_auth()
.with_cert_resolver(resolver);
let rustls_config = RustlsConfig::from_config(Arc::new(server_config));
ak_axum::server::start_tls(
tasks,
"proxy-outpost",
router.clone(),
addr,
rustls_config,
)?;
}
Ok(())
}
#[instrument(skip_all)]
async fn refresh(&self) -> Result<()> {
debug!(
outpost_pk = %self.controller.outpost.load().pk,
"requesting providers for outpost"
);
let providers = fetch_all(
|page| {
outposts_proxy_list(
&self.controller.api_config,
None,
None,
Some(page),
Some(100_i32),
None,
)
},
|r| &r.pagination,
|r| r.results,
)
.await
.inspect_err(|err| error!(?err, "failed to fetch providers"))?;
debug!(count = providers.len(), "fetched providers");
if providers.is_empty() && !self.controller.is_embedded() {
warn!(
"no providers assigned to this outpost, check outpost configuration in authentik"
);
}
for (i, provider) in providers.iter().enumerate() {
debug!(
index = i,
name = provider.name,
external_host = provider.external_host,
assigned_to_app = provider.assigned_application_name,
"provider details"
);
}
let mut apps = HashMap::with_capacity(providers.len());
for provider in providers {
let name = provider.name.clone();
let Ok(application) = Application::new(self, provider)
.await
.inspect_err(|err| warn!(?err, "failed to setup application, skipping provider"))
else {
continue;
};
info!(name, host = application.host, "loaded application");
apps.insert(application.host.clone(), Arc::new(application));
}
self.apps.store(Arc::new(apps));
Ok(())
}
async fn end_session(&self, _event: super::event::EventSessionEnd) -> Result<()> {
// todo!()
warn!(?_event, "removing session");
Ok(())
}
}
impl ResolvesServerCert for ProxyOutpost {
fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
if let Some(server_name) = client_hello.server_name()
&& let Some(app) = self.apps.load().get(server_name)
&& let Some(cert) = &app.cert
{
return Some(Arc::clone(cert));
}
Some(Arc::clone(&self.default_cert))
}
fn only_raw_public_keys(&self) -> bool {
false
}
}
impl ProxyOutpost {
#[instrument(skip(self))]
fn lookup_app(&self, host: &str) -> Option<Arc<Application>> {
let apps = self.apps.load();
// If we only have a single app, host name switching doesn't matter.
if apps.len() == 1
&& let Some(app) = apps.values().next()
{
debug!(app = app.provider.name, "found a single app, using it");
return Some(Arc::clone(app));
}
if let Some(app) = apps.get(host) {
debug!(app = app.provider.name, "found app based direct host match");
return Some(Arc::clone(app));
}
// For forward_auth_domain, we don't have a direct app to domain relationship.
// Check through all apps, and check how much of their cookie domain matches the host.
// Return the application that has the longest match.
let mut longest_match = None;
let mut longest_len = 0_usize;
for app in apps.values() {
if app.provider.mode != Some(ProxyMode::ForwardDomain) {
continue;
}
if let Some(cookie_domain) = app.provider.cookie_domain.as_deref() {
// Check if the cookie domain has a leading period for a wildcard.
// This will decrease the weight of a wildcard domain, but a request to example.com
// with the cookie domain set to example.com will still be routed correctly.
let domain = cookie_domain.trim_start_matches('.');
if host.ends_with(domain) && domain.len() > longest_len {
longest_len = domain.len();
longest_match = Some(Arc::clone(app));
}
// For forward_auth_domain, we need to response on the external domain too.
if app.provider.external_host == host {
debug!(app = app.provider.name, "found app based on external_host");
return Some(Arc::clone(app));
}
}
}
if let Some(app) = &longest_match {
debug!(app = app.provider.name, "found app based on cookie domain");
}
longest_match
}
}
fn build_router(outpost: Arc<ProxyOutpost>) -> Router {
wrap_router(
Router::new()
.nest(
"/outpost.goauthentik.io/ping",
Router::new().fallback(handlers::handle_ping),
)
.fallback(handlers::default)
.with_state(outpost),
true,
)
}

View File

@@ -35,41 +35,8 @@ ak-sidebar-item:active ak-sidebar-item::part(list-item) {
background-color: transparent !important;
}
[part="command-palette-trigger"] {
--BackgroundColor: var(--pf-global--BackgroundColor--150);
background: var(--BackgroundColor);
border-radius: var(--pf-global--BorderRadius--sm);
border: 0.5px solid var(--pf-global--BorderColor--100);
.command-palette-trigger {
cursor: pointer;
display: grid;
gap: var(--pf-global--spacer--sm);
grid-template-columns: [icon] auto [label] 1fr [shortcut-hint] auto;
justify-items: start;
align-items: center;
margin-block-end: var(--pf-global--spacer--form-element);
margin-block-start: var(--pf-global--spacer--sm);
margin-inline: var(--pf-global--spacer--sm);
padding-block: var(--pf-global--spacer--form-element);
padding-inline-start: var(--pf-global--spacer--sm);
user-select: none;
z-index: 1;
color: var(--pf-global--Color--200);
padding-inline-end: var(--pf-global--spacer--sm);
position: relative;
.placeholder {
font-style: italic;
font-size: var(--pf-global--FontSize--sm);
}
&:hover {
--BackgroundColor: var(--pf-global--BackgroundColor--200);
}
.icon {
display: block;
height: var(--pf-global--icon--FontSize--md);
fill: currentColor;
stroke: currentColor;
}
}

View File

@@ -269,12 +269,13 @@ export class AdminInterface extends WithCapabilitiesConfig(
<i aria-hidden="true" class="fas fa-bars"></i>
</button>
${this.renderCommandPaletteButton()}
<ak-version-banner></ak-version-banner>
<ak-enterprise-status interface="admin"></ak-enterprise-status>
</ak-page-navbar>
<ak-sidebar ?hidden=${!this.sidebarOpen} class="${classMap(sidebarClasses)}">
${this.renderCommandPaletteButton()} ${renderSidebarItems(this.entries)}
<ak-sidebar ?hidden=${!this.sidebarOpen} class="${classMap(sidebarClasses)}"
>${renderSidebarItems(this.entries)}
${this.can(CapabilitiesEnum.IsEnterprise)
? renderSidebarItems(createAdminSidebarEnterpriseEntries())
: nothing}
@@ -320,31 +321,36 @@ export class AdminInterface extends WithCapabilitiesConfig(
const primaryModifierKey = macOS ? "⌘" : "Ctrl";
return html`<button
slot="before-items"
part="command-palette-trigger"
slot="nav-buttons"
@click=${this.commandPalette.showListener}
type="button"
class="pf-c-button pf-m-plain command-palette-trigger"
aria-label=${msg("Open Command Palette", {
id: "command-palette-trigger-label-mobile",
id: "command-palette-trigger-label",
desc: "Label for the button that opens the command palette",
})}
>
<svg
class="icon"
role="img"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
>
<path
d="m29 27.586-7.552-7.552a11.018 11.018 0 1 0-1.414 1.414L27.586 29ZM4 13a9 9 0 1 1 9 9 9.01 9.01 0 0 1-9-9"
/>
</svg>
<div class="placeholder">${msg("Search...")}</div>
<div class="ak-c-kbd">
<kbd>${primaryModifierKey}</kbd> <span class="ak-c-kbd__plus">+</span>
<kbd>K</kbd>
</div>
<pf-tooltip position="top-end">
<div slot="content" class="ak-tooltip__content--inline">
${msg("Open Command Palette", {
id: "command-palette-trigger-tooltip",
desc: "Tooltip for the button that opens the command palette",
})}
<div class="ak-c-kbd"><kbd>${primaryModifierKey}</kbd> + <kbd>K</kbd></div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
class="ak-c-vector-icon"
role="img"
viewBox="0 0 32 32"
>
<path
d="M26 4.01H6a2 2 0 0 0-2 2v20a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-20a2 2 0 0 0-2-2m0 2v4H6v-4Zm-20 20v-14h20v14Z"
/>
<path d="m10.76 16.18 2.82 2.83-2.82 2.83 1.41 1.41 4.24-4.24-4.24-4.24z" />
</svg>
</pf-tooltip>
</button>`;
});
}

View File

@@ -81,7 +81,7 @@ export class DeviceListPage extends TablePage<EndpointDevice> {
renderSectionBefore() {
return html`
<div class="pf-c-banner pf-m-info ak-m-inset">
<div class="pf-c-banner pf-m-info">
${msg("Endpoint Devices are in preview.")}
<a href="mailto:hello+feature/platform@goauthentik.io"
>${msg("Send us feedback!")}</a

View File

@@ -13,7 +13,7 @@ export class LifecyclePreviewBanner extends AKElement {
static styles = [PFBanner];
public render(): TemplateResult {
return html`<div class="pf-c-banner pf-m-info ak-m-inset">
return html`<div class="pf-c-banner pf-m-info">
${msg("Object Lifecycle Management is in preview.")}
<a href="mailto:hello+feature/lifecycle@goauthentik.io">${msg("Send us feedback!")}</a>
</div>`;

View File

@@ -37,7 +37,7 @@ export class AuthenticatorEndpointGDTCStageForm extends BaseStageForm<Authentica
static styles = [...super.styles, PFBanner];
protected override renderForm(): TemplateResult {
return html`<div class="pf-c-banner pf-m-info ak-m-inset">
return html`<div class="pf-c-banner pf-m-info">
${msg("Endpoint Google Chrome Device Trust is in preview.")}
<a href="mailto:hello+feature/gdtc@goauthentik.io">${msg("Send us feedback!")}</a>
</div>

View File

@@ -35,6 +35,7 @@
.pf-c-nav__list {
flex-grow: 1;
overflow-y: auto;
padding-block-start: var(--pf-global--spacer--sm);
}
.pf-c-nav__link.pf-m-current::after,

View File

@@ -20,22 +20,10 @@ export class Sidebar extends AKElement {
];
@property({ type: Boolean })
public hidden = false;
protected defaultSlot: HTMLSlotElement;
protected beforeItemsSlot: HTMLSlotElement;
constructor() {
super();
this.defaultSlot = this.ownerDocument.createElement("slot");
this.beforeItemsSlot = this.ownerDocument.createElement("slot");
this.beforeItemsSlot.name = "before-items";
}
hidden = false;
render(): TemplateResult {
return html`<div part="nav" class="pf-c-nav" role="presentation">
${this.beforeItemsSlot}
<ul
id="global-nav"
?hidden=${this.hidden}
@@ -44,7 +32,7 @@ export class Sidebar extends AKElement {
class="pf-c-nav__list ak-m-thin-scrollbar ak-m-scroll-shadows"
part="list"
>
${this.defaultSlot}
<slot></slot>
</ul>
<ak-sidebar-version
exportparts="trigger:about-dialog-trigger, button-content:about-dialog-button-content, product-name, product-version"

View File

@@ -91,19 +91,18 @@
/* #region Keyboard */
.ak-c-kbd {
--ak-c-kbd--ShadowColor: var(--pf-global--BackgroundColor--dark-transparent-200);
--ak-c-kbd--InsetColor: var(--pf-global--BackgroundColor--100);
--ak-c-kbd--ShadowColor: var(--pf-global--BackgroundColor--dark-transparent-100);
--ak-c-kbd--InsetColor: var(--pf-global--Color--light-300);
display: flex;
align-items: center;
line-height: 1;
gap: 0.125em;
kbd {
user-select: none;
text-rendering: optimizeLegibility;
letter-spacing: 0.05em;
padding: 0.25rem 0.5rem;
padding: 0.25rem 0.25rem;
border-radius: 3px;
box-shadow:
0 1px 1px var(--ak-c-kbd--ShadowColor),
@@ -113,15 +112,9 @@
font-weight: bold;
line-height: 1;
white-space: nowrap;
background-color: var(--pf-global--BackgroundColor--150);
color: var(--pf-global--Color--200);
background-color: var(--pf-global--BackgroundColor--light-100);
color: var(--pf-global--Color--dark-100);
font-family: var(--pf-global--FontFamily--monospace);
text-align: center;
}
.ak-c-kbd__plus {
font-size: var(--pf-global--FontSize--sm);
color: var(--pf-global--Color--dark-200);
}
}

View File

@@ -13,14 +13,6 @@
}
}
.pf-c-banner.ak-m-inset {
margin-block-start: calc(
var(--pf-global--spacer--xs) + (var(--pf-global--spacer--form-element) / 2)
);
margin-inline: var(--pf-global--spacer--md);
border-radius: var(--pf-global--BorderRadius--sm);
}
:host([theme="dark"]) .pf-c-banner {
&.pf-m-info,
&.pf-m-blue,

View File

@@ -10,10 +10,6 @@
--pf-c-page__sidebar--m-dark--BackgroundColor: var(--pf-global--BackgroundColor--100);
--pf-c-page__sidebar--BackgroundColor: var(--pf-c-page__sidebar--m-light--BackgroundColor);
--pf-c-page__main-section--xl--PaddingTop: var(--pf-global--spacer--md);
--pf-c-page__main-section--xl--PaddingLeft: var(--pf-global--spacer--md);
--pf-c-page__main-section--xl--PaddingRight: var(--pf-global--spacer--md);
--pf-c-page__main-section--xl--PaddingBottom: var(--pf-global--spacer--md);
}
.pf-c-page__drawer {