Compare commits

...

11 Commits

Author SHA1 Message Date
Marc 'risson' Schmitt
4d141dd331 wip
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-13 15:14:30 +02:00
Marc 'risson' Schmitt
6848eb744b extract some logic from lookup app for future use with embedded outpost
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-13 15:14:30 +02:00
Marc 'risson' Schmitt
2aab203559 move to cert store
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-13 15:14:30 +02:00
Marc 'risson' Schmitt
780b500402 on start and signal, reload immediately, don't wait for reload offset
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-13 15:14:29 +02:00
Marc 'risson' Schmitt
f63328aa1f tls certificates
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-13 15:14:29 +02:00
Marc 'risson' Schmitt
986656407f wip
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-13 15:14:29 +02:00
Marc 'risson' Schmitt
4eeaebfcdb start on application router
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-13 15:14:28 +02:00
Marc 'risson' Schmitt
1914c2d4de continue on handlers
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-13 15:14:28 +02:00
Marc 'risson' Schmitt
49f609e5f8 wip
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-13 15:14:28 +02:00
Marc 'risson' Schmitt
7f2a3a1524 add container
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2026-05-13 15:14:28 +02:00
Marc 'risson' Schmitt
e574c6439f 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-13 15:14:27 +02:00
17 changed files with 1266 additions and 39 deletions

185
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,18 @@ dependencies = [
"nix 0.31.2",
"pyo3",
"pyo3-build-config",
"rand 0.10.1",
"rustls",
"serde",
"serde_json",
"serde_repr",
"sqlx",
"time",
"tokio",
"tokio-tungstenite",
"tower",
"tracing",
"url",
"uuid",
"which",
]
@@ -241,12 +292,14 @@ dependencies = [
"config",
"console-subscriber",
"eyre",
"futures",
"glob",
"ipnet",
"json-subscriber",
"nix 0.31.2",
"notify",
"pin-project-lite",
"rcgen",
"reqwest",
"reqwest-middleware",
"rustls",
@@ -264,6 +317,7 @@ dependencies = [
"tracing-error",
"tracing-subscriber",
"url",
"uuid",
]
[[package]]
@@ -542,6 +596,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 +844,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 +947,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 +1379,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasip2",
"wasip3",
]
@@ -2172,6 +2261,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"
@@ -2401,6 +2500,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"
@@ -2802,6 +2910,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"
@@ -2840,6 +2959,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"
@@ -2867,6 +2992,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"
@@ -3030,6 +3168,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"
@@ -3409,7 +3556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -3420,7 +3567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -3990,8 +4137,12 @@ checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
dependencies = [
"futures-util",
"log",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tungstenite",
"webpki-roots 0.26.11",
]
[[package]]
@@ -4205,8 +4356,11 @@ dependencies = [
"httparse",
"log",
"rand 0.9.4",
"rustls",
"rustls-pki-types",
"sha1",
"thiserror 2.0.18",
"url",
]
[[package]]
@@ -5077,6 +5231,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"
@@ -5088,6 +5260,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.3", 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.10", features = ["timeout"] }
@@ -260,28 +269,40 @@ 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-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

@@ -22,11 +22,13 @@ axum-server.workspace = true
config-rs.workspace = true
console-subscriber.workspace = true
eyre.workspace = true
futures.workspace = true
glob.workspace = true
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
@@ -43,6 +45,7 @@ tracing-error.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
url.workspace = true
uuid.workspace = true
[dev-dependencies]
nix.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,9 @@ use tracing::trace;
use crate::config;
pub mod self_signed;
pub mod store;
/// 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

@@ -0,0 +1,92 @@
use std::{collections::HashMap, sync::Arc};
use ak_client::apis::{
configuration::Configuration,
crypto_api::{
crypto_certificatekeypairs_retrieve, crypto_certificatekeypairs_view_certificate_retrieve,
crypto_certificatekeypairs_view_private_key_retrieve,
},
};
use eyre::{Report, Result};
use futures::FutureExt as _;
use rustls::{
crypto::CryptoProvider,
pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject as _},
sign::CertifiedKey,
};
use tokio::sync::Mutex;
use uuid::Uuid;
#[derive(Debug)]
pub struct Certificate {
pub fingerprint: String,
pub certificate: String,
pub key: String,
pub certified_key: Arc<CertifiedKey>,
}
#[derive(Clone, Debug, Default)]
pub struct CertificateStore {
certificates: Arc<Mutex<HashMap<Uuid, Arc<Certificate>>>>,
}
impl CertificateStore {
pub fn new() -> Self {
Self::default()
}
pub async fn ensure_keypair(
&self,
api_config: &Configuration,
kp_uuid: Uuid,
) -> Result<Arc<Certificate>> {
let kp_uuid_s = kp_uuid.to_string();
let fingerprint = crypto_certificatekeypairs_retrieve(api_config, &kp_uuid_s)
.await?
.fingerprint_sha256;
if let Some(certificate) = self.certificates.lock().await.get(&kp_uuid)
&& let Some(fingerprint) = &fingerprint
&& &certificate.fingerprint == fingerprint
{
return Ok(Arc::clone(certificate));
}
let (cert, key) = tokio::try_join!(
crypto_certificatekeypairs_view_certificate_retrieve(api_config, &kp_uuid_s, None,)
.map(|res| res.map_err(Report::from)),
crypto_certificatekeypairs_view_private_key_retrieve(api_config, &kp_uuid_s, None,)
.map(|res| res.map_err(Report::from)),
)?;
let certified_key = {
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");
Arc::new(CertifiedKey::new(
cert_chain,
provider.key_provider.load_private_key(key_der)?,
))
};
let cert = Arc::new(Certificate {
fingerprint: fingerprint.unwrap_or_default(),
certificate: cert.data,
key: key.data,
certified_key,
});
if !cert.fingerprint.is_empty() {
self.certificates
.lock()
.await
.insert(kp_uuid, Arc::clone(&cert));
}
Ok(cert)
}
}

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;

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

@@ -0,0 +1,318 @@
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,
reload_offset: Option<Duration>,
) -> Result<()> {
match event.instruction {
EventKind::Ack | EventKind::Hello => {}
EventKind::TriggerUpdate => {
info!("received update trigger, refreshing outpost");
if let Some(reload_offset) = reload_offset {
sleep(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
},
None,
).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
},
None,
).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,
Some(controller.reload_offset),
).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,61 @@
use std::sync::Arc;
use ak_client::models::ProxyOutpostConfig;
use ak_common::tls::store::Certificate;
use axum::Router;
use eyre::{Result, eyre};
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<Certificate>>,
}
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();
let _old_app = outpost.apps.load().get(external_host);
let cert = if let Some(Some(kp_uuid)) = provider.certificate {
Some(
outpost
.certificate_store
.ensure_keypair(&outpost.controller.api_config, kp_uuid)
.await?,
)
} 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
};
let router = Router::new();
Ok(Self {
host: external_host.to_owned(),
provider,
router,
cert,
})
}
}

View File

@@ -0,0 +1,87 @@
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 _, debug, 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 app = outpost.lookup_app(&host).or_else(|| {
// If we only have a single app, host name switching doesn't matter.
let apps = outpost.apps.load();
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));
}
None
});
let Some(app) = app 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)
}

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

@@ -0,0 +1,231 @@
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::{self, store::CertificateStore},
};
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>>>,
certificate_store: CertificateStore,
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)),
certificate_store: CertificateStore::new(),
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.certified_key));
}
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 apps.is_empty() {
return None;
}
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,
)
}