v0.34.15
This commit is contained in:
1 parent
62b7574553
commit
e364392659
47 files changed
+2164
-257
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
# RustProxy source and documentation maintenance
|
# RustProxy source and documentation maintenance
|
||||||
|
|
||||||
Current source version: **0.34.14**. The Cargo package and binary are named `rust-proxy`.
|
Current source version: **0.34.15**. The Cargo package and binary are named `rust-proxy`.
|
||||||
|
|
||||||
Before editing, read [README.md](README.md), [ROADMAP.md](ROADMAP.md), the relevant [documentation guide](docs/README.md) and the actual code and tests. Historical release experiments are not current specifications. Use the bundled `src/admin/openapi.json` for API routes and payloads; the source implementation is authoritative for runtime behavior.
|
Before editing, read [README.md](README.md), [ROADMAP.md](ROADMAP.md), the relevant [documentation guide](docs/README.md) and the actual code and tests. Historical release experiments are not current specifications. Use the bundled `src/admin/openapi.json` for API routes and payloads; the source implementation is authoritative for runtime behavior.
|
||||||
|
|
||||||
@@ -12,6 +12,13 @@ Before editing, read [README.md](README.md), [ROADMAP.md](ROADMAP.md), the relev
|
|||||||
- Keep per-component optional defaults accurate (for example COLD off, Prometheus off, backend `tls_verify` off unless explicitly set, and optional Web Server `sendfile` off). Mark source-only or untested changes accordingly.
|
- Keep per-component optional defaults accurate (for example COLD off, Prometheus off, backend `tls_verify` off unless explicitly set, and optional Web Server `sendfile` off). Mark source-only or untested changes accordingly.
|
||||||
- Maintain existing `langs/*.json` packs; `build.rs` discovers available locales. Do not add credentials, databases, real certificates, environment secrets or build artifacts to source archives.
|
- Maintain existing `langs/*.json` packs; `build.rs` discovers available locales. Do not add credentials, databases, real certificates, environment secrets or build artifacts to source archives.
|
||||||
|
|
||||||
|
## Authentication provider extension contract
|
||||||
|
|
||||||
|
- Keep provider implementations isolated under `src/<provider>/mod.rs` and `tests.rs`; the request pipeline only sees the object-safe `auth_provider::Provider` trait and the single `CompiledFrontend.auth_provider` slot. Preserve existing Vhost JSON fields on upgrades.
|
||||||
|
- Prefer the shared `forward_auth::Adapter` transport for compatible HTTP verifiers, and reserve protected-host callback routes through `reserved_path` / `serve_reserved` only when required.
|
||||||
|
- Return fail-closed provider errors using `auth_provider::gateway_failure` and one `src/admin/web/error_auth_provider.html` template (with escaped provider names); never create a separate HTML file per vendor or expose backend diagnostics to clients. `AuthenticationResponse` prevents custom Vhost error pages from overriding auth errors/redirects, and `Identity` / `CookieSink` keep identities and cookies request-scoped.
|
||||||
|
- Update the Vhost selector, mutual-exclusion validation, config compiler, docs and provider tests together. Prove access/denial, redirects, header stripping, timeouts, and reserved routes with real verifiers before deployment.
|
||||||
|
|
||||||
## Engineering safeguards
|
## Engineering safeguards
|
||||||
|
|
||||||
- Keep proxy traffic independent of control-plane database/provider calls. Prefer asynchronous streaming, bounded queues, resource limits and explicit cancellation. Hold backend permits for the entire body or tunnel lifetime.
|
- Keep proxy traffic independent of control-plane database/provider calls. Prefer asynchronous streaming, bounded queues, resource limits and explicit cancellation. Hold backend permits for the entire body or tunnel lifetime.
|
||||||
|
|||||||
+9
-1
@@ -1,6 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
Release history focused on **features, settings, default behavior, compatibility and fixes**. Changes to internal code structure and preliminary performance experiments are intentionally omitted. Historical descriptions reflect their respective releases; consult the current documentation for the settings available in 0.34.14.
|
Release history focused on **features, settings, default behavior, compatibility and fixes**. Changes to internal code structure and preliminary performance experiments are intentionally omitted. Historical descriptions reflect their respective releases; consult the current documentation for the settings available in 0.34.15.
|
||||||
|
|
||||||
|
## 0.34.15 — optional Authelia and authentik forward auth (source candidate)
|
||||||
|
|
||||||
|
- **Provider-neutral refactor:** Extracted `auth_provider::Provider` with unified selected-provider dispatch, verified identity/cookie handling and one `error_auth_provider.html` template. Removed three Tinyauth-only HTML template files; 502/504 titles still identify Tinyauth, Authelia or authentik and failure category. Shared `forward_auth` transport continues to be reused without embedding vendor-specific branching in the request pipeline.
|
||||||
|
- **Vhost → Authentication:** two additional, independently configurable choices: Authelia (`/api/authz/forward-auth`) and authentik (`/outpost.goauthentik.io/auth/nginx`), alongside unchanged Tinyauth and HTTP Basic Auth. Exactly one method can be active per Vhost; both new methods default off, with old configurations remaining compatible. Settings can be edited through the panel and vhosts configuration API.
|
||||||
|
- **Authelia:** bounded server-side verification, trusted original-request metadata, allowlisted identity headers and strict same-portal-origin login redirects. The Authelia portal and session cookies must be configured for each protected parent domain; a single portal URL cannot issue a valid browser session cookie for unrelated parent domains.
|
||||||
|
- **authentik:** supports forward auth (single application) with each protected host's reserved `/outpost.goauthentik.io` route automatically proxied to the configured outpost. The protected URL is passed to the outpost for SSO login; request cookies, identity headers and session-refresh `Set-Cookie` values are propagated with limits. Different parent domains can use one central authentik instance with distinct per-app proxy providers.
|
||||||
|
- **Fail closed:** verifier transport/invalid-response errors return 502; timeout returns 504; unauthenticated/denied requests never reach application origins. Provider identity is never accepted from incoming client headers. Cache sharing is disabled for protected requests; fail-closed auth responses are kept out of custom-error interception.
|
||||||
|
|
||||||
## 0.34.14 — Tinyauth gateway error pages (source candidate)
|
## 0.34.14 — Tinyauth gateway error pages (source candidate)
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -1925,7 +1925,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-proxy"
|
name = "rust-proxy"
|
||||||
version = "0.34.14"
|
version = "0.34.15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rust-proxy"
|
name = "rust-proxy"
|
||||||
version = "0.34.14"
|
version = "0.34.15"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.99"
|
rust-version = "1.99"
|
||||||
description = "RustProxy - independent reverse proxy with local administration"
|
description = "RustProxy - independent reverse proxy with local administration"
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
# RustProxy 0.34.14
|
# RustProxy 0.34.15
|
||||||
|
|
||||||
RustProxy combines a reverse proxy, static/PHP web server, certificate management and traffic monitoring in one service with a browser administration panel. It supports several domains and backends without requiring a separate proxy configuration language.
|
RustProxy combines a reverse proxy, static/PHP web server, certificate management and traffic monitoring in one service with a browser administration panel. It supports several domains and backends without requiring a separate proxy configuration language.
|
||||||
|
|
||||||
|
## Changes in 0.34.15
|
||||||
|
|
||||||
|
- Added **Authelia** and **authentik** as separate, disabled-by-default Vhost authentication methods alongside existing Tinyauth and HTTP Basic Auth. Each Vhost selects at most one provider; older Vhost JSON imports keep their settings.
|
||||||
|
- Authelia integration uses `/api/authz/forward-auth` and validates login redirects against the configured public portal. authentik integration uses `/outpost.goauthentik.io/auth/nginx`, redirects unauthenticated clients to the protected hostname's outpost and forwards that reserved path to the configured outpost server.
|
||||||
|
- Authentication checks fail closed on provider outages, malformed responses and timeouts. Verifier-returned identity headers are selected/limited, client-forged identity headers are stripped, and successful sessions remain private/no-store. Protected application domains may differ when each provider is configured appropriately.
|
||||||
|
|
||||||
## Changes in 0.34.14
|
## Changes in 0.34.14
|
||||||
|
|
||||||
- Preserves Tinyauth Nginx auth-request compatibility with old and new versions supporting `/api/auth/nginx`.
|
- Preserves Tinyauth Nginx auth-request compatibility with old and new versions supporting `/api/auth/nginx`.
|
||||||
@@ -14,7 +20,7 @@ RustProxy combines a reverse proxy, static/PHP web server, certificate managemen
|
|||||||
| Reverse proxy | HTTP/HTTPS origins, HTTP/1.1 and HTTP/2 upstream modes, health checks, load balancing, connection reuse, WebSockets, path-based routing, retries and error fallbacks |
|
| Reverse proxy | HTTP/HTTPS origins, HTTP/1.1 and HTTP/2 upstream modes, health checks, load balancing, connection reuse, WebSockets, path-based routing, retries and error fallbacks |
|
||||||
| Frontends | HTTP, HTTPS, HTTP/2, optional HTTP/3 (QUIC), named listeners, domain and wildcard routing, redirects, TLS certificates |
|
| Frontends | HTTP, HTTPS, HTTP/2, optional HTTP/3 (QUIC), named listeners, domain and wildcard routing, redirects, TLS certificates |
|
||||||
| Web Server | Static content, downloads, directory/index policy, rewrite and `try_files`, selected `.htaccess` rules, optional PHP-FPM, application presets |
|
| Web Server | Static content, downloads, directory/index policy, rewrite and `try_files`, selected `.htaccess` rules, optional PHP-FPM, application presets |
|
||||||
| Access | IP/CIDR and DNS/DDNS rules, country restrictions, reusable ACLs, rate limits, method rules, Basic Auth, Tinyauth, security headers |
|
| Access | IP/CIDR and DNS/DDNS rules, country restrictions, reusable ACLs, rate limits, method rules, Basic Auth, Tinyauth, Authelia and authentik, security headers |
|
||||||
| Delivery | Optional shared HOT/RAM and COLD/disk cache, cache invalidation, compression, response transformations, static-file optimizations |
|
| Delivery | Optional shared HOT/RAM and COLD/disk cache, cache invalidation, compression, response transformations, static-file optimizations |
|
||||||
| Integrations | Docker discovery via separate agents, TCP/UDP Streams, ACME certificate providers, Prometheus, Grafana/Nagios examples and administration API |
|
| Integrations | Docker discovery via separate agents, TCP/UDP Streams, ACME certificate providers, Prometheus, Grafana/Nagios examples and administration API |
|
||||||
| Operations | Dashboard presets, traffic analytics, live/stored logs, configuration history, encrypted backups and certificate reloads |
|
| Operations | Dashboard presets, traffic analytics, live/stored logs, configuration history, encrypted backups and certificate reloads |
|
||||||
|
|||||||
+45
-38
@@ -1,12 +1,12 @@
|
|||||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||||
9a882c75ba505b097de65a1073accd3ccb30395b2454f3c3fb9b25c7b2d9bd6e AGENTS.md
|
0fac1a1a51c2b28a35ab3ca5ca78bc622d923f6338d060ddd28b024e0c585c94 AGENTS.md
|
||||||
3e27930459e3225a19aa5174bb9523df3613c009b7b4fe117311b528f740ec61 CHANGELOG.md
|
0add2dca4bcce8a7b52aedaa91b80cb828055d19df3236803a6cf6a10702bea9 CHANGELOG.md
|
||||||
11b42e2c29662b09b6ff1a575ec45a98856daa6cbc2c0351afa9f1fa4a11b1b5 Cargo.lock
|
9990725b3ad88f93526c87d09cebc2505b3b34db7dcdaa1fdc5e40e5352891a0 Cargo.lock
|
||||||
f3cc334705c920f910dbc0fce374b7223c2e6a66abe614cf8074c6e89da1d537 Cargo.toml
|
0b93f835f1a5edc78105fcd2c2fdae02d48badab667089ec67543a93dee80b9f Cargo.toml
|
||||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||||
750f73027faed6039b06f75f87c5a3c1e646ea396a64750688d2ca1344ab72bf README.md
|
0230d86529bc60fa2db5ececff0ce3763fd44675f5ba3f48b6187bff27b474d6 README.md
|
||||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ROADMAP.md
|
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ROADMAP.md
|
||||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||||
ac07b3bdd46c787989e93f55ebd6ea3d7f5b150ef36f2422e2b58df3f6b7543a build.rs
|
ac07b3bdd46c787989e93f55ebd6ea3d7f5b150ef36f2422e2b58df3f6b7543a build.rs
|
||||||
@@ -27,9 +27,9 @@ d660f46f7ef8c539dd3f5486a31d5a1254b9737be8a278c7f9352c254558f8ac docker-agent/m
|
|||||||
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
|
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
|
||||||
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
||||||
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
||||||
93aa788dc1205bc9386a95a39b30a3862a42ba6a2e90e82fc5bfe681fc7db6a7 docs/ACCESS.md
|
e34ce67e7105be1e3a666beaaf28338b6ce443c18a6cabdf9e54cb7e4ce61963 docs/ACCESS.md
|
||||||
147755fb33e9e91098a4d2dfbe6df8b0d92d602e002d568e64a0f815641be2c2 docs/ACME_PROVIDERS.md
|
147755fb33e9e91098a4d2dfbe6df8b0d92d602e002d568e64a0f815641be2c2 docs/ACME_PROVIDERS.md
|
||||||
ba40dd0ec51e0992c0fecd12934b9875e5e7252eced8db533cb79ec01ac8dcc7 docs/API.md
|
0489f81f1a766850b12c69a44b9fc4caef9a7441eccf4ed854df67d9989e7d41 docs/API.md
|
||||||
2362ab34020d4e0dcd70e280e9539ef7764b63212856ee08a3a55906c48f6421 docs/API_ACCESS.md
|
2362ab34020d4e0dcd70e280e9539ef7764b63212856ee08a3a55906c48f6421 docs/API_ACCESS.md
|
||||||
728b15b9825f547f34c4121219784798dd046fbbdd72ece8906d0cd77e8efc64 docs/BACKUP.md
|
728b15b9825f547f34c4121219784798dd046fbbdd72ece8906d0cd77e8efc64 docs/BACKUP.md
|
||||||
3219aae15fd2e10157343ecdad8b175bd9735f39bccb31a5cb811553c13084dd docs/CACHE.md
|
3219aae15fd2e10157343ecdad8b175bd9735f39bccb31a5cb811553c13084dd docs/CACHE.md
|
||||||
@@ -39,18 +39,18 @@ dc670d912a6a946160c2f8528c9bb710ec38ebe406dcb7dcaf18fb2653f667b6 docs/CUSTOM_LO
|
|||||||
e350c5081085b79e579d1500fa07feee94a709adaf6dfde7b3d9af124a78d184 docs/DASHBOARD.md
|
e350c5081085b79e579d1500fa07feee94a709adaf6dfde7b3d9af124a78d184 docs/DASHBOARD.md
|
||||||
11a08bf2110bbeabc924eb049a30177a216af5607ccdaf8be9178e751f78a2c4 docs/DOCKER_DISCOVERY.md
|
11a08bf2110bbeabc924eb049a30177a216af5607ccdaf8be9178e751f78a2c4 docs/DOCKER_DISCOVERY.md
|
||||||
c891ee5272e2e47e1a8f97133a413e2330844103d543fb5cbcc4d0724d59a1a5 docs/INSTALL.md
|
c891ee5272e2e47e1a8f97133a413e2330844103d543fb5cbcc4d0724d59a1a5 docs/INSTALL.md
|
||||||
7c97df0b89e230851fe5211f5b295aac1f71a0f64385bf165d2aa7db893fdf74 docs/INTEGRATIONS.md
|
27f48388891a040a4895428749d7e95fda620ab45ea3d68c536645a5bd5d89f2 docs/INTEGRATIONS.md
|
||||||
694cb7b15c5bbdc94cecd15dc516c05dcb3467849322cdd125b872d36dee4699 docs/LOGS.md
|
694cb7b15c5bbdc94cecd15dc516c05dcb3467849322cdd125b872d36dee4699 docs/LOGS.md
|
||||||
888cdc8c56e10b8a7615012e192e085bf2899d0ec9b3ddfb5731e7de6ab5bcde docs/MIGRATION.md
|
888cdc8c56e10b8a7615012e192e085bf2899d0ec9b3ddfb5731e7de6ab5bcde docs/MIGRATION.md
|
||||||
27ba3ed337963d9cd05ba382666eed9c414f58c52e5897b4c1c45603f412b456 docs/MONITORING.md
|
27ba3ed337963d9cd05ba382666eed9c414f58c52e5897b4c1c45603f412b456 docs/MONITORING.md
|
||||||
d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.md
|
d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.md
|
||||||
0f66cef23c9a8caa378497787811edcb4f48e31b72bbd958b2862a9549d78077 docs/README.md
|
8d29408a99926e3c2ae76f52b8a41c76b80447b953bcba4df242ea8e54bd0d5d docs/README.md
|
||||||
490a6dbe870f2b1fd4ec7816768dde6c86fa7957fc9de2d2f075e123c9222222 docs/RESPONSE_DELIVERY.md
|
490a6dbe870f2b1fd4ec7816768dde6c86fa7957fc9de2d2f075e123c9222222 docs/RESPONSE_DELIVERY.md
|
||||||
a7c5b01ad1e6aeb27bccf1085504854b70ed4bf020b481915aafcc4a8aeb1f54 docs/SECURITY.md
|
a7c5b01ad1e6aeb27bccf1085504854b70ed4bf020b481915aafcc4a8aeb1f54 docs/SECURITY.md
|
||||||
bbef60ff1771da9f48af812987766e1e479408873ae2f079b13f1d5c62ecc6ff docs/STREAMS.md
|
bbef60ff1771da9f48af812987766e1e479408873ae2f079b13f1d5c62ecc6ff docs/STREAMS.md
|
||||||
6508819f181e2a935ffa117451915aa5568a31e2890c57fa10e04543db82f3e7 docs/TLS_POST_QUANTUM.md
|
6508819f181e2a935ffa117451915aa5568a31e2890c57fa10e04543db82f3e7 docs/TLS_POST_QUANTUM.md
|
||||||
43a2c6230f95f81bf4efd1575a4f7cdc8b02244df38e19b1ec9c3cd736d3afe6 docs/TRANSPORT.md
|
43a2c6230f95f81bf4efd1575a4f7cdc8b02244df38e19b1ec9c3cd736d3afe6 docs/TRANSPORT.md
|
||||||
df0d5e9e83ec3827ecf6887801137569118f8f29680b0dda23887723f8d4e030 docs/TROUBLESHOOTING.md
|
af1098e461c39c3f0ae09b8dbdbc4c1c362b2662f29c62602399de16f8912e97 docs/TROUBLESHOOTING.md
|
||||||
e14c02f2c95daf5779d1aa5300751162b63fdd968832a1f0c2f191fa00ace6dc docs/VHOSTS.md
|
e14c02f2c95daf5779d1aa5300751162b63fdd968832a1f0c2f191fa00ace6dc docs/VHOSTS.md
|
||||||
7e7eab2ba76c262e4000184f80f9a5cb69c7896fe7a046c0d3d84dc211c4b751 docs/WEB_SERVER.md
|
7e7eab2ba76c262e4000184f80f9a5cb69c7896fe7a046c0d3d84dc211c4b751 docs/WEB_SERVER.md
|
||||||
5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json
|
5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json
|
||||||
@@ -64,7 +64,7 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
|
|||||||
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
|
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
|
||||||
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
|
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
|
||||||
1b8974af79a5026e0648ceab58a32f233e1760e21e7465a0d46e9c41fb09964f grafana/README.md
|
1b8974af79a5026e0648ceab58a32f233e1760e21e7465a0d46e9c41fb09964f grafana/README.md
|
||||||
e775dc98b44bb1c362d8de85ffdee105694807a946029544aa13780c806aac33 grafana/rustproxy-overview.json
|
0d389faa4f64f095e9ec62cbae329658682b96f5e4d02ead79c7d2892ab89fa8 grafana/rustproxy-overview.json
|
||||||
a05cbbf101b288bf9cf83fedfc5017925a8b360a78f313d31c1a70f15dc1dd22 install-debian13.sh
|
a05cbbf101b288bf9cf83fedfc5017925a8b360a78f313d31c1a70f15dc1dd22 install-debian13.sh
|
||||||
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
|
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
|
||||||
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
|
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
|
||||||
@@ -77,8 +77,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
|
|||||||
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
|
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
|
||||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
||||||
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
|
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
|
||||||
0aa1109be04a8a3e32a3aebed25ae25054e4a44574d92556eccff1424e31c148 langs/en_US.json
|
956566ae2aff162cca55961bd1a92341746f0fb8e7187d8167bcbd06677a0c2c langs/en_US.json
|
||||||
1fcc95afa3f7374bb6a9105004877ee207ee6fec1b616fc72ee8f832054756bd langs/pl_pl.json
|
9d9385ca67e5fd01f3b27b21d5515adc87ecda53129cda71130bff00f809157a langs/pl_pl.json
|
||||||
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
|
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
|
||||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||||
@@ -149,7 +149,7 @@ d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file
|
|||||||
282352eda7957477e7da44f16b90984b26a4a67a8755833bc0a6b34eaf93e3af src/admin/file_logs_query.rs
|
282352eda7957477e7da44f16b90984b26a4a67a8755833bc0a6b34eaf93e3af src/admin/file_logs_query.rs
|
||||||
444d7715406ffd2305a72decf7fdf633aa4c2752cd881c8fa3b95ed971d79820 src/admin/logging_api.rs
|
444d7715406ffd2305a72decf7fdf633aa4c2752cd881c8fa3b95ed971d79820 src/admin/logging_api.rs
|
||||||
243f4a9c3fa99781e19c3952354d2632c0ce65e1dafac729567cbe3c177f94cb src/admin/mod.rs
|
243f4a9c3fa99781e19c3952354d2632c0ce65e1dafac729567cbe3c177f94cb src/admin/mod.rs
|
||||||
cf7393a5a6c101bc739a4cbf73a2abf60ca8fe2184ec84ae244ef9bd5f99c2e1 src/admin/openapi.json
|
8ac7bd4270241e1a9834146cd9ace181a1facb72e70c1837a10d07c261bf7fde src/admin/openapi.json
|
||||||
1f4ec45d146e00ef664fae37517f8fcb5455d47daec496c02b2fc15ddec49131 src/admin/operations.rs
|
1f4ec45d146e00ef664fae37517f8fcb5455d47daec496c02b2fc15ddec49131 src/admin/operations.rs
|
||||||
257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs
|
257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs
|
||||||
f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs
|
f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs
|
||||||
@@ -161,7 +161,7 @@ b3c3c2d33c150eb42ce210c6074e5eff6a8af9391118012e413bc9323d3ae50c src/admin/pref
|
|||||||
6930b4f4b45257772b321b20a9becdf9bc4610274852948ba38c2d0cdf02dfaf src/admin/refresh_preferences.rs
|
6930b4f4b45257772b321b20a9becdf9bc4610274852948ba38c2d0cdf02dfaf src/admin/refresh_preferences.rs
|
||||||
cc3aa1b094b0d869d141754ac2877817ec40a362acfd7fc3167ea3c9f1ff1a18 src/admin/router.rs
|
cc3aa1b094b0d869d141754ac2877817ec40a362acfd7fc3167ea3c9f1ff1a18 src/admin/router.rs
|
||||||
e135431cb078ddb483165581123c64af8f00ea67acb3a395ec3e53c85d0d0d45 src/admin/session.rs
|
e135431cb078ddb483165581123c64af8f00ea67acb3a395ec3e53c85d0d0d45 src/admin/session.rs
|
||||||
09cc50a86960897371730cc1abed83f4f640c86b680ec7ff6d6b84c7e96a3c0b src/admin/status.rs
|
7aeb784a3ef58795c8fe26467bb4e1a00e61420911f798a584ab09c12f73a754 src/admin/status.rs
|
||||||
2d5b4c28fd0bd48b06890fa8ec44a2296de948c1f88eb2909f4b495710d4e13e src/admin/status_live.rs
|
2d5b4c28fd0bd48b06890fa8ec44a2296de948c1f88eb2909f4b495710d4e13e src/admin/status_live.rs
|
||||||
8315c856cd50f1072593ecb30c274d42d82f078b5fb93e7c68a8857cd81b0841 src/admin/status_system.rs
|
8315c856cd50f1072593ecb30c274d42d82f078b5fb93e7c68a8857cd81b0841 src/admin/status_system.rs
|
||||||
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
||||||
@@ -194,7 +194,7 @@ b7ead49224a4ec67181d3f24f9d9634bd6d76756f826a4d5d993300637d1d8f3 src/admin/web/
|
|||||||
e9788a9d1e0bce8775f612a5c842afea8a0dcf01301acdef4b69ff6a52d2ef45 src/admin/web/certificate-api.js
|
e9788a9d1e0bce8775f612a5c842afea8a0dcf01301acdef4b69ff6a52d2ef45 src/admin/web/certificate-api.js
|
||||||
9d000e98497b3387641500d5273d23eda45d532c714f22e187572928da152a97 src/admin/web/css/00-tokens.css
|
9d000e98497b3387641500d5273d23eda45d532c714f22e187572928da152a97 src/admin/web/css/00-tokens.css
|
||||||
51437cbc5ec2d9690a685f9ad1c9235e503049adc4438e1b10ed232a9909b862 src/admin/web/css/10-base.css
|
51437cbc5ec2d9690a685f9ad1c9235e503049adc4438e1b10ed232a9909b862 src/admin/web/css/10-base.css
|
||||||
195a6af7f54f310d936d32b1074e3c5daeaac1a8c687b8dff0223be8ea34eb36 src/admin/web/css/20-shell.css
|
be10487cdaf89acfb7b4cf893d82580ac4c64ac68cb3094d349fc8de10c61d6c src/admin/web/css/20-shell.css
|
||||||
707d08b7b8505c7346d5a98f3a8058f10577ddac7b9e6a5a9894285bde9e300a src/admin/web/css/30-overview.css
|
707d08b7b8505c7346d5a98f3a8058f10577ddac7b9e6a5a9894285bde9e300a src/admin/web/css/30-overview.css
|
||||||
ce5ffb911d7e4cd2317ca047534921132b6f93efd70a26673f0c428e55320a4d src/admin/web/css/40-editors.css
|
ce5ffb911d7e4cd2317ca047534921132b6f93efd70a26673f0c428e55320a4d src/admin/web/css/40-editors.css
|
||||||
6537a3f97d046b8325c744a281a74f8c4005a8bbcd97284ecf7eb5b6973ae1df src/admin/web/css/50-settings.css
|
6537a3f97d046b8325c744a281a74f8c4005a8bbcd97284ecf7eb5b6973ae1df src/admin/web/css/50-settings.css
|
||||||
@@ -222,9 +222,7 @@ f2cb26eec86b6c03151072f847153df8aea612d840ca8b76367f41b9605bce64 src/admin/web/
|
|||||||
ff72fc4c6a11907a16234c4b3c7d7f7bcf82eb72868dc3fbf654b04aff806fc8 src/admin/web/error_502.html
|
ff72fc4c6a11907a16234c4b3c7d7f7bcf82eb72868dc3fbf654b04aff806fc8 src/admin/web/error_502.html
|
||||||
386b9dc3aecebe7727f7cf7bbd2a02ab036a3abf01315975111a51559128df73 src/admin/web/error_503.html
|
386b9dc3aecebe7727f7cf7bbd2a02ab036a3abf01315975111a51559128df73 src/admin/web/error_503.html
|
||||||
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
|
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
|
||||||
ce598fadd521ee000f05f7d29d70cdeca684a53dbd946a38af204fb2a9248b1b src/admin/web/error_tinyauth_connection.html
|
003d382cc8dd69ad7237679632c04a9389366bb71dd7dca8ee64ee06614aa9a5 src/admin/web/error_auth_provider.html
|
||||||
daa9ef52d96b21c492380486d1cf476c715dcd6c339b431887cc31776de966c8 src/admin/web/error_tinyauth_timeout.html
|
|
||||||
026361d7c3c3b596daa290feaa734f042fabdf236369cd4bebc39795f0c4fc96 src/admin/web/error_tinyauth_verification.html
|
|
||||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||||
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
|
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
|
||||||
3f0525ca1d4e088ccd4988b8a117b26a8cf8c12213b24c37081d41b68d2b0ff2 src/admin/web/host404.js
|
3f0525ca1d4e088ccd4988b8a117b26a8cf8c12213b24c37081d41b68d2b0ff2 src/admin/web/host404.js
|
||||||
@@ -244,11 +242,11 @@ a8afc6e70950bc70b98725eb049d73cff35dc3a3ee659229a15a8e08d3eb4f12 src/admin/web/
|
|||||||
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
||||||
c7e0ca45c19498d40ef6f4122de23f0e73094e905d39f798942f70d15b9cf86e src/admin/web/transform-panel.js
|
c7e0ca45c19498d40ef6f4122de23f0e73094e905d39f798942f70d15b9cf86e src/admin/web/transform-panel.js
|
||||||
a55f45dad5b540def419f8487161fb506b7e52744c76a5e661c7a5fa0a98a426 src/admin/web/vhosts/core.js
|
a55f45dad5b540def419f8487161fb506b7e52744c76a5e661c7a5fa0a98a426 src/admin/web/vhosts/core.js
|
||||||
d88d89c8071d2458aef278498a5a7863fb45c374fca801b0b3a9de402c455b4c src/admin/web/vhosts/editor.js
|
d4e447c176a72a6f803f84c6a6925c4905acbcd31ea5c9bf7bc3ed9e0f455054 src/admin/web/vhosts/editor.js
|
||||||
8338ce13b5e3e3f421437631124cb691aeb5b9e17409d256b4b9bcac457b3c59 src/admin/web/vhosts/health.js
|
8338ce13b5e3e3f421437631124cb691aeb5b9e17409d256b4b9bcac457b3c59 src/admin/web/vhosts/health.js
|
||||||
b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/vhosts/protocol.js
|
b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/vhosts/protocol.js
|
||||||
8c3da49973c62333559b0e3588c02beb0b425c025eac5cfa82a1bb968f70d9dd src/admin/web/vhosts/rates.js
|
8c3da49973c62333559b0e3588c02beb0b425c025eac5cfa82a1bb968f70d9dd src/admin/web/vhosts/rates.js
|
||||||
03f99423df8b32aed674478afa24f6b366fb3128d9bab56218dcef71456bedc4 src/admin/web/vhosts/render.js
|
cbfcaff4f83ed3058556ed8247e5d9f1ee16aee1772dde770c7bdacb699dc5d6 src/admin/web/vhosts/render.js
|
||||||
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
|
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
|
||||||
dafce3a59a6007e59d7a4018fce96a14cd8e8cedb3ce77f02ee229a280c122d8 src/admin/web/web-server.js
|
dafce3a59a6007e59d7a4018fce96a14cd8e8cedb3ce77f02ee229a280c122d8 src/admin/web/web-server.js
|
||||||
2d40c2cb32d7365df0c9cd6c8cb6f0483c7a799786c648f4836a0d9740c6c5ef src/auth/files/mod.rs
|
2d40c2cb32d7365df0c9cd6c8cb6f0483c7a799786c648f4836a0d9740c6c5ef src/auth/files/mod.rs
|
||||||
@@ -256,6 +254,13 @@ b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files
|
|||||||
50ef648351271d78310e00caa6d246e0009171ed036a3e4e1144737df8c064c2 src/auth/mod.rs
|
50ef648351271d78310e00caa6d246e0009171ed036a3e4e1144737df8c064c2 src/auth/mod.rs
|
||||||
25b9f78a7eee277902d375266bd0f023b79794bcdf646214fc161c5e6dab72d1 src/auth/store.rs
|
25b9f78a7eee277902d375266bd0f023b79794bcdf646214fc161c5e6dab72d1 src/auth/store.rs
|
||||||
745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs
|
745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs
|
||||||
|
514e99e0d3da21abd633d53872234bc4d7d0178583ab291e53cf4b1802fd5337 src/auth_provider/errors.rs
|
||||||
|
fffd302d2b4f5fa6575a09eae428445403375c5f26c29e4b4818f5f263918637 src/auth_provider/mod.rs
|
||||||
|
72a6e245cef078585bc39932033c05cd866ee518e9cb4e7b89bca87ce941e971 src/auth_provider/testing.rs
|
||||||
|
59e1af728770d28265198844b3dd5740328bcb1df96d170aae575d3d97ac42d8 src/authelia/mod.rs
|
||||||
|
f44b10fd3286b805008f746bee8164bd9c3ec7ebc86724314408f6703d6e1f41 src/authelia/tests.rs
|
||||||
|
0137e66fb7127f1ce023b1c6cfe3fac3fb1e8be17f1de722f8ef4e66d2594a13 src/authentik/mod.rs
|
||||||
|
fb244b3f44f9f4ba0be8c7572f43510afa0af7bb9c47759d0e9a93c8a96e1616 src/authentik/tests.rs
|
||||||
32a25e53ac9542424debeaf1b637155bd4e60abd8692df082729d81232eb90b3 src/backup.rs
|
32a25e53ac9542424debeaf1b637155bd4e60abd8692df082729d81232eb90b3 src/backup.rs
|
||||||
0f07cac0259e849773466a6fd468fbe84ea34bdb79745dae8ace286b8c707727 src/backup/archive.rs
|
0f07cac0259e849773466a6fd468fbe84ea34bdb79745dae8ace286b8c707727 src/backup/archive.rs
|
||||||
15213c87d4393739328ebf0eacfcb45f864ac12d7bd7509f750a241032f6c7e9 src/backup/ownership.rs
|
15213c87d4393739328ebf0eacfcb45f864ac12d7bd7509f750a241032f6c7e9 src/backup/ownership.rs
|
||||||
@@ -270,7 +275,7 @@ a3d052a928f73ea430480637b867ae874d447e24b1ac913a96a068a4dd74ff63 src/balance/po
|
|||||||
477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs
|
477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs
|
||||||
d789bf40cf9433e2fd16409d112210fe93154588b1ac9bddc9c7f78e1db14426 src/balance/pool_view.rs
|
d789bf40cf9433e2fd16409d112210fe93154588b1ac9bddc9c7f78e1db14426 src/balance/pool_view.rs
|
||||||
3e75a3cba5f1cd1af996ab71b6ad50f6aa05ac9cd97ee3cf88fce7c6b7271fbe src/balance/tests.rs
|
3e75a3cba5f1cd1af996ab71b6ad50f6aa05ac9cd97ee3cf88fce7c6b7271fbe src/balance/tests.rs
|
||||||
16a22aa72f311d1fea6d8da03c3ea1590bc1f248b21876354eecc0da0fd93308 src/basic_auth/mod.rs
|
ca1699c931c9d80c3ba42f4f70f5c7260bf69ba3af40a64639311d461cf91e62 src/basic_auth/mod.rs
|
||||||
9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs
|
9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs
|
||||||
76b7db53f38c95e128fee503381efd3fdcc13340c124ef277faf0fe5c1c25c39 src/cache/cache_regression_tests.rs
|
76b7db53f38c95e128fee503381efd3fdcc13340c124ef277faf0fe5c1c25c39 src/cache/cache_regression_tests.rs
|
||||||
7def03ace07dc38a4806277d75b6ce673d33ddaaf1c4855fa0a3372be1cf9489 src/cache/cold.rs
|
7def03ace07dc38a4806277d75b6ce673d33ddaaf1c4855fa0a3372be1cf9489 src/cache/cold.rs
|
||||||
@@ -318,11 +323,11 @@ e695079333ebdc2a6e7b3337cfabef171b3a0a01da0597b77feff70d417f833e src/config.rs
|
|||||||
9f6272bd6d1f403cf6324b12472fd5c7d911a387139b57105ba1819e48ea0a54 src/config/backend.rs
|
9f6272bd6d1f403cf6324b12472fd5c7d911a387139b57105ba1819e48ea0a54 src/config/backend.rs
|
||||||
f4df024acdb24be450822e58108af23db3dc82bf1f77531cd16fe340fc01062c src/config/compile.rs
|
f4df024acdb24be450822e58108af23db3dc82bf1f77531cd16fe340fc01062c src/config/compile.rs
|
||||||
fbea01fea1213d98aea812c1b78d5db289e09200b98ae514d742235cd3fa70e1 src/config/internal_routing.rs
|
fbea01fea1213d98aea812c1b78d5db289e09200b98ae514d742235cd3fa70e1 src/config/internal_routing.rs
|
||||||
6e8116efb074397905b41331097cbf81099a21a896b686b4de2e3f210e783620 src/config/model.rs
|
101253b4c9275e38dc7e1088051d6cd55ae7616213eb1c02e3e1c5ffc6fea4a0 src/config/model.rs
|
||||||
75839458ddbc7480ec4492a95eeb67b617ba7d62f7b67e7a767a3453b74dbcbc src/config/route_policy_tests.rs
|
75839458ddbc7480ec4492a95eeb67b617ba7d62f7b67e7a767a3453b74dbcbc src/config/route_policy_tests.rs
|
||||||
40e9483d7b2450f55d29b46c408727f4e331b85ecda44fbd7b96bd9a09f02655 src/config/routing.rs
|
f9db8dc17edadb1d549fed8b10f858a0bf1d0d51802df7d558f7bbf33c1c1486 src/config/routing.rs
|
||||||
dd9a6ad8f58e0cdd1ae74f3f972fee0ac14383cd8b8f518403b9a36c169f35bd src/config/snapshot.rs
|
dd9a6ad8f58e0cdd1ae74f3f972fee0ac14383cd8b8f518403b9a36c169f35bd src/config/snapshot.rs
|
||||||
ba5829a5fb65e76e2e3450e897eb2d21105809f1d5e16b630a6faca27ca14252 src/config/tests.rs
|
094dc6688d5bd0e8bef92f17736bad740a95b4765cd29fe89d5362d155e868c5 src/config/tests.rs
|
||||||
f35de0ee1516ece6bbfd2b2a7db7d1e0f5048b0ec47cf2a3765dffb799abfd93 src/config/transport_tests.rs
|
f35de0ee1516ece6bbfd2b2a7db7d1e0f5048b0ec47cf2a3765dffb799abfd93 src/config/transport_tests.rs
|
||||||
e0f46d885ed81ce306a110560cdd2c217dadde054986d89fcec835ebf6939d65 src/db.rs
|
e0f46d885ed81ce306a110560cdd2c217dadde054986d89fcec835ebf6939d65 src/db.rs
|
||||||
c290d0cb02dfa5d2c60ecabac1cfb20c8884e3d14cca1944906c6d8e677f1784 src/db/api_tokens.rs
|
c290d0cb02dfa5d2c60ecabac1cfb20c8884e3d14cca1944906c6d8e677f1784 src/db/api_tokens.rs
|
||||||
@@ -337,6 +342,8 @@ f9e8539223b4f1d254ee960f59cd4dcf719627101dd9e0190fcd7d331b674fa4 src/diagnostic
|
|||||||
f69087a82cf353e9a92fc97ee96510dbe0382efa1c5e53821d16c455a3eb95a4 src/docker/model.rs
|
f69087a82cf353e9a92fc97ee96510dbe0382efa1c5e53821d16c455a3eb95a4 src/docker/model.rs
|
||||||
810415a436d842b79fa72392cc18977ad9c368d233ce43e135a60119722c9e77 src/docker/reconcile.rs
|
810415a436d842b79fa72392cc18977ad9c368d233ce43e135a60119722c9e77 src/docker/reconcile.rs
|
||||||
2e8d4841665cc1d29e6262a1789bb64ced1cecd0036681cc1ef7b5d5bfe22065 src/docker/tests.rs
|
2e8d4841665cc1d29e6262a1789bb64ced1cecd0036681cc1ef7b5d5bfe22065 src/docker/tests.rs
|
||||||
|
468ad6df59092d97e79b42e4387460488040b5b7ea99c7639946f799c6863966 src/forward_auth/mod.rs
|
||||||
|
6c25951cf69e972c47cdf815f22a5062916b2b768e39d55a26a5fc9e625a2344 src/forward_auth/tests.rs
|
||||||
b496edbfcb8fe7875a95835f8b10a0dbd46775227dbd1e5fee555b284c836216 src/forwarding.rs
|
b496edbfcb8fe7875a95835f8b10a0dbd46775227dbd1e5fee555b284c836216 src/forwarding.rs
|
||||||
08d2e08a798abb944a53570f0bdaa08f1266b1107677ceda070091af4c2fc121 src/forwarding/peer_formatting.rs
|
08d2e08a798abb944a53570f0bdaa08f1266b1107677ceda070091af4c2fc121 src/forwarding/peer_formatting.rs
|
||||||
b821e2efd2939cb13d01e402532f33feba6682acb6a7a3223be086c1cd171820 src/forwarding/tests.rs
|
b821e2efd2939cb13d01e402532f33feba6682acb6a7a3223be086c1cd171820 src/forwarding/tests.rs
|
||||||
@@ -355,7 +362,7 @@ cfc75486e69c310fa6e7bf0448ed39a20cdad9dd8afe75e01592fc7c76a380db src/geoip/test
|
|||||||
49ce4452ef7973cafa2e4071e2e14be0e16000e931a6501e4dc613652e49e230 src/http3/identity.rs
|
49ce4452ef7973cafa2e4071e2e14be0e16000e931a6501e4dc613652e49e230 src/http3/identity.rs
|
||||||
0b41072f6bcf28725c80f471584cd55d4900424ce5d55ac7f6909813788c63fd src/http3/settings.rs
|
0b41072f6bcf28725c80f471584cd55d4900424ce5d55ac7f6909813788c63fd src/http3/settings.rs
|
||||||
5c550075c7b0fe687ad02095c1cad57b46b579fe21dc5371525b6c77db459886 src/http3/tests.rs
|
5c550075c7b0fe687ad02095c1cad57b46b579fe21dc5371525b6c77db459886 src/http3/tests.rs
|
||||||
65cbe42886c89cd6675fe42f2051a8adba6b22d4bd106d93a205321baefd7a01 src/lib.rs
|
894bd4b19323d28969d53b1fd9e095cdb4a7a54ee3b6d9e4a41f4fe1d1eaf757 src/lib.rs
|
||||||
e8399753a6ff49b7de19edb0a268bf871c9979a518354160cf07b4669051fcbb src/logging.rs
|
e8399753a6ff49b7de19edb0a268bf871c9979a518354160cf07b4669051fcbb src/logging.rs
|
||||||
e4be3ad758c3d93ce52dd3b1ab1a87810e5226339400ed997607a5b746354751 src/logging/error_runtime.rs
|
e4be3ad758c3d93ce52dd3b1ab1a87810e5226339400ed997607a5b746354751 src/logging/error_runtime.rs
|
||||||
b2ed730006fdc080cf7eafc6d20bb89661c639631c89642d110e0c1319e431b5 src/logging/format.rs
|
b2ed730006fdc080cf7eafc6d20bb89661c639631c89642d110e0c1319e431b5 src/logging/format.rs
|
||||||
@@ -377,7 +384,7 @@ ee401f943bd8b2706c33419ed69102269c08dcb4ceba6f4be2ac0861115874a7 src/metrics/vh
|
|||||||
9c96c3e4edb20dd0c36b5c373f1925dd60368e75853002206523072d418d5417 src/not_found/mod.rs
|
9c96c3e4edb20dd0c36b5c373f1925dd60368e75853002206523072d418d5417 src/not_found/mod.rs
|
||||||
fa2fc6c6987a8356a9da92cade10db62426d8863e9cef3c0d4324c45780c5b33 src/not_found/tests.rs
|
fa2fc6c6987a8356a9da92cade10db62426d8863e9cef3c0d4324c45780c5b33 src/not_found/tests.rs
|
||||||
d32d757ace8753eccb842c24b7af77061141478d10fe0acdfb05263d19e9a64c src/pages/mod.rs
|
d32d757ace8753eccb842c24b7af77061141478d10fe0acdfb05263d19e9a64c src/pages/mod.rs
|
||||||
0e973bf65f232b5de00f0f39b349a6fe9cec7aa1760184e2474e0c7bde52f7be src/pages/tests.rs
|
a3474253e83d1548bdafcb696a80f064283062196d665bb6316952355d54b785 src/pages/tests.rs
|
||||||
5650a3f70162abd97021e0a51a92cac2c4dd39004426ef99dff36cbb0741aeac src/prometheus.rs
|
5650a3f70162abd97021e0a51a92cac2c4dd39004426ef99dff36cbb0741aeac src/prometheus.rs
|
||||||
192a780d5874afc158d62a7dbe1687d734635afe9342fff526b8a2f6db4e5633 src/prometheus/core.rs
|
192a780d5874afc158d62a7dbe1687d734635afe9342fff526b8a2f6db4e5633 src/prometheus/core.rs
|
||||||
2321ee667569803dfe6c92c0e98adba0ac8fcdc6a3eacd5857233f1acae93daf src/prometheus/docker_misc.rs
|
2321ee667569803dfe6c92c0e98adba0ac8fcdc6a3eacd5857233f1acae93daf src/prometheus/docker_misc.rs
|
||||||
@@ -389,22 +396,22 @@ cdbfa0e7f49200dc70ea6df8f0594399ee3e0c5fc3d279cc336e2d83d0f8a535 src/prometheus
|
|||||||
f1b0254f53b1a4336bd8170ad441ba072b49a114e497b985cb20699b4e375044 src/prometheus/web_php.rs
|
f1b0254f53b1a4336bd8170ad441ba072b49a114e497b985cb20699b4e375044 src/prometheus/web_php.rs
|
||||||
569eda61453affd053fbe24fa59652289cb12d22b77e64caa38325bbea48f7df src/proxy/body/mod.rs
|
569eda61453affd053fbe24fa59652289cb12d22b77e64caa38325bbea48f7df src/proxy/body/mod.rs
|
||||||
5beeac12ca7f041bdb9b74b2dbc9781f5d5524fcf51294b8b6ad88284754ef20 src/proxy/body/tests.rs
|
5beeac12ca7f041bdb9b74b2dbc9781f5d5524fcf51294b8b6ad88284754ef20 src/proxy/body/tests.rs
|
||||||
e5e32d86780119dd5eec4c8a369cb1aea44904ab62646e04e458640a3e4358eb src/proxy/cache_helpers.rs
|
e97894a7ca8a3c4dd753c8d4d1bff20cfa50f8380d8864b8cd4197241c018040 src/proxy/cache_helpers.rs
|
||||||
78caad08c548386958f605f82f6c4299ab94a910a1332a1e0817dc53920c96a3 src/proxy/cache_http_invalidation.rs
|
78caad08c548386958f605f82f6c4299ab94a910a1332a1e0817dc53920c96a3 src/proxy/cache_http_invalidation.rs
|
||||||
a86b10ce0707b51e925e11ea14f7f40fa108a196166f62cf40007a97f9e17203 src/proxy/cache_http_invalidation_tests.rs
|
a86b10ce0707b51e925e11ea14f7f40fa108a196166f62cf40007a97f9e17203 src/proxy/cache_http_invalidation_tests.rs
|
||||||
a03b1735daa8fa8e40cf5bd9c6e200d211baf19c30f62522d4237ed56642bce7 src/proxy/cache_plan/mod.rs
|
a03b1735daa8fa8e40cf5bd9c6e200d211baf19c30f62522d4237ed56642bce7 src/proxy/cache_plan/mod.rs
|
||||||
11f11fd8131023d7bfeceb6f4cbf169db994e2b6ca1931167f96ae590d8e0e01 src/proxy/cache_plan/tests.rs
|
62da703feff7e51bddc8998cb4d459f3ddbb2c109874285fa27fc920030b9129 src/proxy/cache_plan/tests.rs
|
||||||
3ba479d72d0e94263acaf2eb84f4de0ff2b0b455ee6a4d3f04bb869a5a30a103 src/proxy/error_fallback.rs
|
3ba479d72d0e94263acaf2eb84f4de0ff2b0b455ee6a4d3f04bb869a5a30a103 src/proxy/error_fallback.rs
|
||||||
aacc3249b3cbce0726dc16e28ee09d78efc4ac89103675287b1adab870bb4e15 src/proxy/forward/cache_fast_path_tests.rs
|
aacc3249b3cbce0726dc16e28ee09d78efc4ac89103675287b1adab870bb4e15 src/proxy/forward/cache_fast_path_tests.rs
|
||||||
f7903200756dc6393392560656b23364f0d444d6357d264713b17c63fd8b636b src/proxy/forward/cache_lookup.rs
|
f7903200756dc6393392560656b23364f0d444d6357d264713b17c63fd8b636b src/proxy/forward/cache_lookup.rs
|
||||||
287012c81144d8f15af4a148ce3ca911d431bc59adff44e697567561aa372bb3 src/proxy/forward/h3_navigation_tests.rs
|
287012c81144d8f15af4a148ce3ca911d431bc59adff44e697567561aa372bb3 src/proxy/forward/h3_navigation_tests.rs
|
||||||
a6580dc359d1f377a48ccc1a902056dc5c2ef86c89180b9b0995068d9817ae8a src/proxy/forward/response.rs
|
a6580dc359d1f377a48ccc1a902056dc5c2ef86c89180b9b0995068d9817ae8a src/proxy/forward/response.rs
|
||||||
1a7322aadbe90e2a6940a27882c13e274b1c92f14de3ebad717054a9614d4ba8 src/proxy/forward/route_cache.rs
|
afcb095b0511072b8cfc197e13f2cd84454ab699155fa964646b8ee60ef00f81 src/proxy/forward/route_cache.rs
|
||||||
0e6e288fb8a0575aaa4e3bcb5c47ed5412a584386a2b00fb25db8428d652cbf3 src/proxy/forward/success_finish.rs
|
0e6e288fb8a0575aaa4e3bcb5c47ed5412a584386a2b00fb25db8428d652cbf3 src/proxy/forward/success_finish.rs
|
||||||
744daef016d4a6c821ab12eeedaa2f29e2de72e2ac9dae16481d4f43476cb713 src/proxy/forward/success_prepare.rs
|
744daef016d4a6c821ab12eeedaa2f29e2de72e2ac9dae16481d4f43476cb713 src/proxy/forward/success_prepare.rs
|
||||||
e9ac9be64f6f83c4ca2cd5d299649f24dd16197712083c6dc10e55c662c65b80 src/proxy/forward/upstream.rs
|
e9ac9be64f6f83c4ca2cd5d299649f24dd16197712083c6dc10e55c662c65b80 src/proxy/forward/upstream.rs
|
||||||
690ff6a6860a7ca8ecd502657a68e5012b7f176036d6d7033dabc514735dc2f5 src/proxy/forward/web_source.rs
|
a6fb336b22c011e7e0936c430d372d20bc77d48ddf0aa491da98843d15f1a126 src/proxy/forward/web_source.rs
|
||||||
ffbddc99112148dd28b4a81ec0527f608ce89fb412a0a8e99e26942cef13f9b5 src/proxy/forward_entry.rs
|
b4121b28db988ad338c52321436bd276d1c69865d2c18d2c1d1eef1e5b86909d src/proxy/forward_entry.rs
|
||||||
9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs
|
9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs
|
||||||
684ea1e2a7591a17e3c633576fc66e2a30d6636738fbce61dab5af44f2c51a67 src/proxy/headers/authority_tests.rs
|
684ea1e2a7591a17e3c633576fc66e2a30d6636738fbce61dab5af44f2c51a67 src/proxy/headers/authority_tests.rs
|
||||||
66efc52d64d34667ba7419f8a2da89d2efb1af1f426684f4fff8caccfdf19996 src/proxy/headers/mod.rs
|
66efc52d64d34667ba7419f8a2da89d2efb1af1f426684f4fff8caccfdf19996 src/proxy/headers/mod.rs
|
||||||
@@ -415,7 +422,7 @@ da9148ce6a60226954cc839f606465b8c16a44806f4d5b2e22ad60484509b967 src/proxy/redi
|
|||||||
de1763b89d98607e20f0f6e3c9b546483927cbe5450efc2e5634fe691fd52ad6 src/proxy/request_id/mod.rs
|
de1763b89d98607e20f0f6e3c9b546483927cbe5450efc2e5634fe691fd52ad6 src/proxy/request_id/mod.rs
|
||||||
78bbc1263bcd600527bf71acdf5af92bf186267a9084b076d1f75ad6492df659 src/proxy/request_id/tests.rs
|
78bbc1263bcd600527bf71acdf5af92bf186267a9084b076d1f75ad6492df659 src/proxy/request_id/tests.rs
|
||||||
580fefdc7dfb27da73f86f4d2067d46a1f856d2f398b0598d37a743e880d3dac src/proxy/types.rs
|
580fefdc7dfb27da73f86f4d2067d46a1f856d2f398b0598d37a743e880d3dac src/proxy/types.rs
|
||||||
37b20f962563babd3602d21ed6326946635281c0b0d97ca59f4e33de170ae93f src/proxy/upstream_helpers.rs
|
faf7296f045fb3a748fa1c21024a2fe54b692a415a59c15992ea54e032b8c0ce src/proxy/upstream_helpers.rs
|
||||||
3fb4896f2ddf9dad783005e34e6d4ca0032fb5a69ee38865b5e0ecc9d7f97f30 src/proxy/web_cache_revalidation/mod.rs
|
3fb4896f2ddf9dad783005e34e6d4ca0032fb5a69ee38865b5e0ecc9d7f97f30 src/proxy/web_cache_revalidation/mod.rs
|
||||||
5522456cc85948228250feac92308d53b130c4a7af98d24f669aca4dfe6999b6 src/proxy/web_cache_revalidation/tests.rs
|
5522456cc85948228250feac92308d53b130c4a7af98d24f669aca4dfe6999b6 src/proxy/web_cache_revalidation/tests.rs
|
||||||
3d591201595a3b1e928ded20a8736477a47e5acc3bcbde158f7c17547ce155ba src/proxy/websocket.rs
|
3d591201595a3b1e928ded20a8736477a47e5acc3bcbde158f7c17547ce155ba src/proxy/websocket.rs
|
||||||
@@ -435,7 +442,7 @@ e53c40b85792d4804a432e3c97dc13af8e29304bb0d250ee9ab9018bde522eeb src/redirect/m
|
|||||||
eb2b0eeeb617cad704732d87acb667530144836e26edb0189929b1f25f35c6b5 src/runtime/docker.rs
|
eb2b0eeeb617cad704732d87acb667530144836e26edb0189929b1f25f35c6b5 src/runtime/docker.rs
|
||||||
18f7460e3738386e070f66932cd77240001255447a769d6e19a40573254a1473 src/runtime/external_config.rs
|
18f7460e3738386e070f66932cd77240001255447a769d6e19a40573254a1473 src/runtime/external_config.rs
|
||||||
5999d7e91ee4231da88c5e9c6d8a12ea9f92d20d6dcdb24319f9f90891d7253b src/runtime/tests.rs
|
5999d7e91ee4231da88c5e9c6d8a12ea9f92d20d6dcdb24319f9f90891d7253b src/runtime/tests.rs
|
||||||
d7a65497d1faa2f09a09c0a0ef77b444990cfb5ab379c85f6c27c1f9c0c143cc src/runtime/types.rs
|
bab3044f4ad8957b65476a0c307bb8a364825b91d2100233b7bd6efa52e771be src/runtime/types.rs
|
||||||
11e99eb425887ad12b73d949f4b497fabc6897fee8fc7647fcb3766269c2fdc0 src/runtime/updates.rs
|
11e99eb425887ad12b73d949f4b497fabc6897fee8fc7647fcb3766269c2fdc0 src/runtime/updates.rs
|
||||||
e3dbd60953db5a976985dd796e4268b41a0f3df59764c4664015bd41e2578058 src/secret_store/mod.rs
|
e3dbd60953db5a976985dd796e4268b41a0f3df59764c4664015bd41e2578058 src/secret_store/mod.rs
|
||||||
6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs
|
6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs
|
||||||
@@ -462,8 +469,8 @@ c02e85af45590866f20933561f85ea9b4f2ead62c2ce41db75113632670a7b5f src/stream/mod
|
|||||||
d9123c623e2a97d09e41edaa29993b4b2c8cba39a3abdf2b48a205aa9a9c2939 src/system_stats/linux/tests.rs
|
d9123c623e2a97d09e41edaa29993b4b2c8cba39a3abdf2b48a205aa9a9c2939 src/system_stats/linux/tests.rs
|
||||||
2c7da96822d605193f0208df70de670612259fd8c762c998ae828848cb210e11 src/system_stats/mod.rs
|
2c7da96822d605193f0208df70de670612259fd8c762c998ae828848cb210e11 src/system_stats/mod.rs
|
||||||
050aa831b1012c4aaade63863f433f01bfacf5d3f95787e57975572116a30ab2 src/system_stats/tests.rs
|
050aa831b1012c4aaade63863f433f01bfacf5d3f95787e57975572116a30ab2 src/system_stats/tests.rs
|
||||||
f0a1109bf7be0dd6a8734be9d47221d99ac993ebfe8f73508191369db69cebcf src/tinyauth/mod.rs
|
41ef9c9ed19a9a0c6742140abdb7c8b8d6d9edc0f828d0cb2ba54eec427696c8 src/tinyauth/mod.rs
|
||||||
e2ab9180f25b1f3c2723ff5a40d35a5a395a188481b766f2730b18e07500a636 src/tinyauth/tests.rs
|
3d40febfbaf38ca2725630eed6b33106e353086081ea811107e4ec9d24f7de11 src/tinyauth/tests.rs
|
||||||
df98becda799b6bf4ff5e2fec7d0e0a486e6b7eca6f9f5ad49ffdafd49a4f498 src/tls.rs
|
df98becda799b6bf4ff5e2fec7d0e0a486e6b7eca6f9f5ad49ffdafd49a4f498 src/tls.rs
|
||||||
2af2164ecdf2b7fd9eef7c6e358dec053dfac9aedaa501dbb75df742d18638b2 src/tls/acme_tests.rs
|
2af2164ecdf2b7fd9eef7c6e358dec053dfac9aedaa501dbb75df742d18638b2 src/tls/acme_tests.rs
|
||||||
84e3da17c59cc90e8575af8158ef4134cd058446a186df675d60106c53dd55ec src/tls/certificates.rs
|
84e3da17c59cc90e8575af8158ef4134cd058446a186df675d60106c53dd55ec src/tls/certificates.rs
|
||||||
@@ -499,10 +506,10 @@ ae880b40197454ea3fbf4d2113984dc372098b2e54b97e87c83cc319de253377 src/upstream/t
|
|||||||
696a2783a24f7f3d69fb820dc3a772ec49ea46ee017087952a8253363a9b7374 src/user_agent/tests.rs
|
696a2783a24f7f3d69fb820dc3a772ec49ea46ee017087952a8253363a9b7374 src/user_agent/tests.rs
|
||||||
d61c427c1e9cf457f1232b822d73e846e9f329cfbe61215e7ca061d53762155d src/vhost.rs
|
d61c427c1e9cf457f1232b822d73e846e9f329cfbe61215e7ca061d53762155d src/vhost.rs
|
||||||
846707452ccf10962f1763be66b8f56c050b0398b48968015fb799dac289c0d6 src/vhost/error_fallback.rs
|
846707452ccf10962f1763be66b8f56c050b0398b48968015fb799dac289c0d6 src/vhost/error_fallback.rs
|
||||||
9d358be766f767d9e1a42f585edcf7fc1cba8f22e5eaac294a83920425ec03c7 src/vhost/model.rs
|
4147d7afbb8f518e774e966a98422b8c03f32a1e792fdf518c0d4e50ded6dcd6 src/vhost/model.rs
|
||||||
fc8af7342a3b147921e9040116af5dfe680b3df910db2416ada18198bbeea7df src/vhost/redirect.rs
|
fc8af7342a3b147921e9040116af5dfe680b3df910db2416ada18198bbeea7df src/vhost/redirect.rs
|
||||||
dd6e121445402a2ac8744f0e4f23d879b4356f47c1bb00737bd15ba459661978 src/vhost/tests.rs
|
dd6e121445402a2ac8744f0e4f23d879b4356f47c1bb00737bd15ba459661978 src/vhost/tests.rs
|
||||||
a69e6138d77ead1558eb9303c68dc317bdfaadbe297fe973d62d4dba41afae73 src/vhost/validation.rs
|
5e48267e62301a2fa3e5a6e9da49223949e73b4ae518fb77d0a62a44a6a8c6d5 src/vhost/validation.rs
|
||||||
8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs
|
8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs
|
||||||
9744e8c29ae72a4d9fbc9a76166a97f8292c4c312295ea40d1cb699e03d80a9d src/web_server/config/compile.rs
|
9744e8c29ae72a4d9fbc9a76166a97f8292c4c312295ea40d1cb699e03d80a9d src/web_server/config/compile.rs
|
||||||
5ed79fa5b9c53f95ca200d89a9ba927dda816b61fbdf3b852bd3c9670e3d2d4f src/web_server/config/model.rs
|
5ed79fa5b9c53f95ca200d89a9ba927dda816b61fbdf3b852bd3c9670e3d2d4f src/web_server/config/model.rs
|
||||||
|
|||||||
+33
-4
@@ -19,7 +19,7 @@ Global restrictions add to per-vhost and path rules. Rate-limit requests are cou
|
|||||||
|
|
||||||
Default deny plus explicit allow exceptions is usually safer for private admin applications. Apply rules to a staging client before enforcing them on a live business-critical service.
|
Default deny plus explicit allow exceptions is usually safer for private admin applications. Apply rules to a staging client before enforcing them on a live business-critical service.
|
||||||
|
|
||||||
In the **Vhost → Security** editor, Traffic policy groups reusable ACL profiles and the separate **inline Vhost IP/CIDR access list** together, followed by HTTP methods, request Content-Type filtering, per-IP rates, country policy, country-rate and User-Agent-rate controls. The inline list is not a second version of an assigned profile: both layers remain independently enforced; an allow in one does not cancel a deny in the other. The **Vhost → Authentication** tab is for Basic Auth and Tinyauth only. HSTS, security headers and User-Agent blocking are separate Security groups. **Custom locations → Access** separates its ACL profile, HTTP method, country and inline IP/CIDR fields. These are only layout changes; restriction scopes still combine as described above.
|
In the **Vhost → Security** editor, Traffic policy groups reusable ACL profiles and the separate **inline Vhost IP/CIDR access list** together, followed by HTTP methods, request Content-Type filtering, per-IP rates, country policy, country-rate and User-Agent-rate controls. The inline list is not a second version of an assigned profile: both layers remain independently enforced; an allow in one does not cancel a deny in the other. The **Vhost → Authentication** tab selects exactly one of Basic Auth, Tinyauth, Authelia and authentik. HSTS, security headers and User-Agent blocking are separate Security groups. **Custom locations → Access** separates its ACL profile, HTTP method, country and inline IP/CIDR fields. These are only layout changes; restriction scopes still combine as described above.
|
||||||
|
|
||||||
## Content-Type request denylist
|
## Content-Type request denylist
|
||||||
|
|
||||||
@@ -29,13 +29,42 @@ Ordinary Vhosts can reject selected MIME types under **Vhosts → Security → R
|
|||||||
|
|
||||||
Method policies allow, block or leave methods unrestricted. A denied method returns `405`, while typical ACL/country denials return `403`. Per-client rate limits can configure request count, window and burst with optional country overrides; exceeding the limit returns `429` with `Retry-After`. Limits are application controls, not a replacement for upstream DDoS protection.
|
Method policies allow, block or leave methods unrestricted. A denied method returns `405`, while typical ACL/country denials return `403`. Per-client rate limits can configure request count, window and burst with optional country overrides; exceeding the limit returns `429` with `Retry-After`. Limits are application controls, not a replacement for upstream DDoS protection.
|
||||||
|
|
||||||
## Basic Auth and Tinyauth
|
## Basic Auth, Tinyauth, Authelia and authentik
|
||||||
|
|
||||||
- **Basic Auth:** protect selected Vhosts with configured credentials. Use HTTPS so credentials are not sent over plaintext.
|
- **Basic Auth:** protect selected Vhosts with configured credentials. Use HTTPS so credentials are not sent over plaintext.
|
||||||
- **Tinyauth:** add a verification URL and an HTTPS login URL. RustProxy makes a bounded, body-free authorization request and can pass specifically allowed identity headers upstream. An origin URL automatically uses `/api/auth/nginx` (or set that path explicitly). This Nginx auth-request integration sends `X-Original-URL` and `X-Original-Method`, with a validated `X-Forwarded-For` and `X-Real-IP` for Tinyauth IP ACLs. It does **not** send `X-Forwarded-Host` or `X-Forwarded-URI` to the verifier, avoiding the `conflicting auth module headers` rejection introduced in Tinyauth 5.2.0 while keeping compatibility with earlier versions that implement the Nginx endpoint. Tinyauth has its own timeout and private-backend policy.
|
- **Tinyauth:** add a verification URL and an HTTPS login URL. RustProxy makes a bounded, body-free authorization request and can pass specifically allowed identity headers upstream. An origin URL automatically uses `/api/auth/nginx` (or set that path explicitly). This Nginx auth-request integration sends `X-Original-URL` and `X-Original-Method`, with a validated `X-Forwarded-For` and `X-Real-IP` for Tinyauth IP ACLs. It does **not** send `X-Forwarded-Host` or `X-Forwarded-URI` to the verifier, avoiding the `conflicting auth module headers` rejection introduced in Tinyauth 5.2.0 while keeping compatibility with earlier versions that implement the Nginx endpoint. Tinyauth has its own timeout and private-backend policy.
|
||||||
- **Tinyauth error pages (0.34.14):** RustProxy renders built-in, self-contained HTML for verifier failures: `502 Tinyauth connection error` if its HTTP client cannot reach Tinyauth (DNS/TCP/TLS/transport), `504 Tinyauth timeout` if the auth deadline expires, or `502 Tinyauth verification error` for unexpected verifier status or invalid/oversized data. The page always retains the HTTP error status, `Content-Type: text/html; charset=utf-8`, `Cache-Control: private, no-store` and `X-Content-Type-Options: nosniff`. Backend URLs, protocol errors and identity data are not shown to visitors. All of these cases **deny access** to the protected origin. HTTP `401`/`403` and configured login redirects remain unchanged. These auth-specific pages take precedence over custom Vhost/general HTTP error pages so the failure remains distinguishable.
|
- **Provider-neutral gateway error pages (0.34.15):** One embedded `src/admin/web/error_auth_provider.html` template renders a self-contained HTML page for **Tinyauth, Authelia, authentik, and future providers**, using only the safe provider display name and the failure category. It replaces the three vendor-specific `error_tinyauth_*.html` source files while preserving the familiar Tinyauth response titles and statuses. For example: `502 Tinyauth connection error` if its HTTP client cannot reach Tinyauth (DNS/TCP/TLS/transport), `504 Tinyauth timeout` if the auth deadline expires, or `502 Tinyauth verification error` for unexpected verifier status or invalid/oversized data. The page always retains the HTTP error status, `Content-Type: text/html; charset=utf-8`, `Cache-Control: private, no-store` and `X-Content-Type-Options: nosniff`. Backend URLs, protocol errors and identity data are not shown to visitors. All of these cases **deny access** to the protected origin. HTTP `401`/`403` and configured login redirects remain unchanged. These auth-specific pages take precedence over custom Vhost/general HTTP error pages so the failure remains distinguishable. The generic failure marker also protects Basic Auth denials and provider login redirects from being replaced by Vhost pages. HTTPS-verifier transport errors are classified as connection failures, truncated/oversized/invalid verifier responses as verification failures, and verification deadlines as timeouts.
|
||||||
- Point the verifier URL **directly** at the private Tinyauth service (for example `http://tinyauth:3000/api/auth/nginx`), rather than routing it through a second public proxy that might inject conflicting headers. Do not disable Tinyauth's header-spoofing protections. Confirm that Tinyauth trusts only the actual RustProxy address/network when enabling Tinyauth IP-based access controls (`TINYAUTH_AUTH_TRUSTEDPROXIES`).
|
- Point the verifier URL **directly** at the private Tinyauth service (for example `http://tinyauth:3000/api/auth/nginx`), rather than routing it through a second public proxy that might inject conflicting headers. Do not disable Tinyauth's header-spoofing protections. Confirm that Tinyauth trusts only the actual RustProxy address/network when enabling Tinyauth IP-based access controls (`TINYAUTH_AUTH_TRUSTEDPROXIES`).
|
||||||
- The two authentication integrations should not be enabled simultaneously for the same Vhost. Protected responses bypass unsafe shared caching. Confirm that login redirections and WebSockets behave as expected.
|
- **Authelia (0.34.15):** select **Authelia** under **Vhost → Authentication**. Enter the internal service origin (for example `http://authelia:9091`) and the public HTTPS login portal (for example `https://auth.example.com`); RustProxy calls `/api/authz/forward-auth`. It forwards trusted `X-Forwarded-*`, `X-Original-URL` and `X-Original-Method` metadata, verifies the provider result, returns provider-approved `302/303` login redirects only to the configured portal origin, and forwards configured `Remote-*` identity headers after stripping client-forged values. A `200` verifier response alone grants access. `401`/`403` deny; transport failures return 502 and timeouts 504. The Authelia `session.cookies` domains and `authelia_url` are defined in Authelia itself: protecting **unrelated registered domains** generally requires a login URL/cookie configuration within each domain, not one cookie from a single unrelated site.
|
||||||
|
- **authentik (0.34.15):** select **authentik** and enter the private outpost origin (for example `http://authentik:9000`), not an `/if/flow` login URL. RustProxy calls `/outpost.goauthentik.io/auth/nginx`, forwards verified `X-Authentik-*` identity headers, and responds to a `401` with a same-origin login start at `/outpost.goauthentik.io/start?rd=...`. RustProxy reserves `/outpost.goauthentik.io` (and descendants) on the protected application hostname and forwards them to the configured outpost **without checking authentication again**, after ordinary global/Vhost IP/country/rate rules. No manual Custom Location is needed for that path. In authentik create a separate **Proxy Provider → Forward auth (single application)** for each protected external application URL, assign it to the serving outpost, then enable that Vhost's integration. Multiple unrelated domains can share the same authentik installation; they do **not** share a browser cookie across domains.
|
||||||
|
- **Adding another provider:** Keep the new vendor's `Settings` and `Compiled` implementation under `src/<provider>/mod.rs` and its tests under `src/<provider>/tests.rs`. Implement the `auth_provider::Provider` trait for runtime authorization, optional `reserved_path`/`serve_reserved` hooks for same-host callbacks, and (for Nginx-compatible providers) the shared `forward_auth::Adapter`. Add an enabled-by-default-false Vhost settings field, validate mutual exclusivity in `auth_provider::Selection`, compile it into the **single** `CompiledFrontend.auth_provider` slot, and register the method in the panel. Reuse `auth_provider::deny` / `auth_provider::gateway_failure` instead of adding separate HTML templates. Existing `tinyauth`, `authelia`, and `authentik` configuration keys remain backward compatible.
|
||||||
|
- **Per-provider security:** The verifier/outpost connection uses TLS certificate verification for HTTPS with optional additional CA, explicit private-backend opt-in and the existing safe-address/local-listener restrictions. Only allowlisted response identity fields and bounded session cookies reach applications/browsers. Inaccessible, malformed or timed-out authorization services **deny** origin access. Both Authelia and authentik are disabled by default; old Tinyauth settings are retained when inactive. Only one of the four authentication methods can be enabled for one Vhost. Protected responses bypass shared caching. Confirm both allowed/denied requests, logins, sessions, redirects and WebSockets with the actual provider before rollout.
|
||||||
|
|
||||||
|
### Minimal Vhost configuration examples (inside a Vhost object)
|
||||||
|
|
||||||
|
Authelia:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"authelia": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "http://authelia.internal:9091",
|
||||||
|
"login_url": "https://auth.example.com",
|
||||||
|
"allow_private": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
authentik (same URL for the outpost check and its reserved public path):
|
||||||
|
|
||||||
|
```json
|
||||||
|
"authentik": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "http://authentik.internal:9000",
|
||||||
|
"allow_private": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `basic_auth.enabled`, `tinyauth.enabled`, and the other provider's `enabled` to `false` for these examples. These are fragments, not whole Vhost documents. Protect the public provider portal with a separate Vhost **without** forward auth to avoid recursive redirects.
|
||||||
|
|
||||||
|
|
||||||
Do not use a User-Agent blocklist as the primary access control: HTTP clients can forge their User-Agent freely.
|
Do not use a User-Agent blocklist as the primary access control: HTTP clients can forge their User-Agent freely.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -78,7 +78,7 @@ The section name in the URL and its corresponding JSON member in a full configur
|
|||||||
| `logging` | `logging`, `error_log` |
|
| `logging` | `logging`, `error_log` |
|
||||||
| `global-security` | `global_security`, including `user_agent_blocking` |
|
| `global-security` | `global_security`, including `user_agent_blocking` |
|
||||||
|
|
||||||
Every GET returns a `value` object or array whose shape can be sent unchanged to the matching PUT. For the three grouped sections the `value` contains exactly the named members; for the other sections it is the serialized value of the matching configuration member. Saving does not alter unrelated sections.
|
Every GET returns a `value` object or array whose shape can be sent unchanged to the matching PUT. For the three grouped sections the `value` contains exactly the named members; for the other sections it is the serialized value of the matching configuration member. Saving does not alter unrelated sections. In 0.34.15 each ordinary Vhost may also configure mutually exclusive `authelia` and `authentik` objects alongside the existing `tinyauth`/`basic_auth` settings; both new providers default to disabled. See [Access](ACCESS.md).
|
||||||
|
|
||||||
### Complete section-update example
|
### Complete section-update example
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ RustProxy offers built-in integrations and optional helper components. Each has
|
|||||||
| Integration | Purpose | Setup and limits |
|
| Integration | Purpose | Setup and limits |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Docker agent** | Discover running Docker services and publish generated Vhosts | Install separate agent on Docker host; use Bearer token and TLS certificate pin. [Guide](DOCKER_DISCOVERY.md). |
|
| **Docker agent** | Discover running Docker services and publish generated Vhosts | Install separate agent on Docker host; use Bearer token and TLS certificate pin. [Guide](DOCKER_DISCOVERY.md). |
|
||||||
| **Tinyauth** | Delegate authentication for selected Vhosts | Configure verifier URL/login URL, allowed headers and HTTPS. Failed verifier connections return a fail-closed local 502 page; verifier timeouts return 504. [Access](ACCESS.md). |
|
| **Tinyauth** | Delegate authentication for selected Vhosts | Existing Nginx auth-request support remains unchanged, with fail-closed 502/504 errors. [Access](ACCESS.md). |
|
||||||
|
| **Authelia** | Forward auth for selected Vhosts | Select it in Vhost Authentication, set internal service origin and trusted public HTTPS login URL, and configure the portal's per-domain cookie rules. [Access](ACCESS.md). |
|
||||||
|
| **authentik** | Forward auth for selected Vhosts, including unrelated application domains | Configure one authentik single-application proxy provider per public application and its serving outpost; enter the outpost's internal origin. RustProxy handles the reserved outpost path automatically. [Access](ACCESS.md). |
|
||||||
| **ACME / Certbot** | Issue/renew HTTPS certificates | HTTP-01 or DNS-01 via Cloudflare, OVH or RFC2136; protect credentials. [Certificates](ACME_PROVIDERS.md). |
|
| **ACME / Certbot** | Issue/renew HTTPS certificates | HTTP-01 or DNS-01 via Cloudflare, OVH or RFC2136; protect credentials. [Certificates](ACME_PROVIDERS.md). |
|
||||||
| **Prometheus** | Scrape operational metrics | Separate endpoint/token/CIDR rules. [Monitoring](MONITORING.md). |
|
| **Prometheus** | Scrape operational metrics | Separate endpoint/token/CIDR rules. [Monitoring](MONITORING.md). |
|
||||||
| **Grafana** | Visualize Prometheus results | Example dashboard under `grafana/README.md`. |
|
| **Grafana** | Visualize Prometheus results | Example dashboard under `grafana/README.md`. |
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
# RustProxy documentation
|
# RustProxy documentation
|
||||||
|
|
||||||
Practical guides for **RustProxy 0.34.14**. This index and the listed pages are also available in the authenticated panel at **`/docs → Reader`**. The adjacent **API** tab provides the embedded OpenAPI reference. Paths below are links to actual files in the source archive.
|
Practical guides for **RustProxy 0.34.15**. This index and the listed pages are also available in the authenticated panel at **`/docs → Reader`**. The adjacent **API** tab provides the embedded OpenAPI reference. Paths below are links to actual files in the source archive.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ Practical guides for **RustProxy 0.34.14**. This index and the listed pages are
|
|||||||
|
|
||||||
- [Vhosts and backends](VHOSTS.md) — domains, listener bindings, request Content-Type filtering, balancing, health and redirects.
|
- [Vhosts and backends](VHOSTS.md) — domains, listener bindings, request Content-Type filtering, balancing, health and redirects.
|
||||||
- [Custom locations](CUSTOM_LOCATIONS.md) — path routing, rewrites, local responses and error fallbacks.
|
- [Custom locations](CUSTOM_LOCATIONS.md) — path routing, rewrites, local responses and error fallbacks.
|
||||||
- [Access and authentication](ACCESS.md) — ACL, country/DDNS, Basic Auth, Tinyauth and rate limits.
|
- [Access and authentication](ACCESS.md) — ACL, country/DDNS, Basic Auth, Tinyauth, Authelia, authentik and rate limits.
|
||||||
- [Static Web Server](WEB_SERVER.md) — document roots, generated filesystem permission commands, downloads, rules, built-in error pages and file delivery.
|
- [Static Web Server](WEB_SERVER.md) — document roots, generated filesystem permission commands, downloads, rules, built-in error pages and file delivery.
|
||||||
- [PHP-FPM](PHP_FPM.md) — PHP service profiles, security and application examples.
|
- [PHP-FPM](PHP_FPM.md) — PHP service profiles, security and application examples.
|
||||||
- [Cache](CACHE.md) — HOT/COLD storage, privacy, TTL, RFC 9211 `Cache-Status`, origin diagnostic headers and invalidation.
|
- [Cache](CACHE.md) — HOT/COLD storage, privacy, TTL, RFC 9211 `Cache-Status`, origin diagnostic headers and invalidation.
|
||||||
@@ -22,7 +22,7 @@ Practical guides for **RustProxy 0.34.14**. This index and the listed pages are
|
|||||||
- [Experimental TLS post-quantum key exchange](TLS_POST_QUANTUM.md) — opt-in X25519MLKEM768, compatibility, rollback and validation.
|
- [Experimental TLS post-quantum key exchange](TLS_POST_QUANTUM.md) — opt-in X25519MLKEM768, compatibility, rollback and validation.
|
||||||
- [TCP/UDP Streams](STREAMS.md) — non-HTTP forwarding and its security boundaries.
|
- [TCP/UDP Streams](STREAMS.md) — non-HTTP forwarding and its security boundaries.
|
||||||
|
|
||||||
The Vhosts and Docker discovery guides include the shared adaptive request-rate chart scale introduced in 0.34.11. Version 0.34.12 preserves that adaptive range and adds a disabled-by-default X/Y scale display option under **Settings → Appearance → Charts**. Release 0.34.13 adds global HTTP security policies and UI layout improvements, groups the global User-Agent policy under `global_security`, provides consistent section-based GET/PUT APIs, and moves manual GeoIP refresh to `POST /api/config/geoip/update`. See [Dashboard](DASHBOARD.md). Release 0.34.14 keeps the Tinyauth Nginx-header compatibility patch, adds fail-closed branded Tinyauth 502/504 error pages, without changing the existing generic 502 page, error-code registry, or other standard error templates. See [Access](ACCESS.md) and [Troubleshooting](TROUBLESHOOTING.md).
|
The Vhosts and Docker discovery guides include the shared adaptive request-rate chart scale introduced in 0.34.11. Version 0.34.12 preserves that adaptive range and adds a disabled-by-default X/Y scale display option under **Settings → Appearance → Charts**. Release 0.34.13 adds global HTTP security policies and UI layout improvements, groups the global User-Agent policy under `global_security`, provides consistent section-based GET/PUT APIs, and moves manual GeoIP refresh to `POST /api/config/geoip/update`. See [Dashboard](DASHBOARD.md). Release 0.34.14 keeps the Tinyauth Nginx-header compatibility patch and dedicated fail-closed 502/504 pages. Release 0.34.15 adds optional Authelia and authentik forward-auth integrations, a shared provider runtime and a single provider-neutral HTML error template. See [Access](ACCESS.md) and [Troubleshooting](TROUBLESHOOTING.md).
|
||||||
|
|
||||||
## Integrations and operations
|
## Integrations and operations
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ Start with a reproducible request, its hostname/path and the exact listener. Com
|
|||||||
| `502 Tinyauth connection error` | The authentication backend cannot be reached. Check Tinyauth process, DNS, verifier URL/port, network firewall, TLS/certificate, private backend allowlist and the RustProxy service journal. Do not route the verification URL back through the protected Vhost. |
|
| `502 Tinyauth connection error` | The authentication backend cannot be reached. Check Tinyauth process, DNS, verifier URL/port, network firewall, TLS/certificate, private backend allowlist and the RustProxy service journal. Do not route the verification URL back through the protected Vhost. |
|
||||||
| `502 Tinyauth verification error` | Tinyauth returned an unexpected status/body or identity headers were too large. Verify `/api/auth/nginx`, proxy header injection and the installed Tinyauth version; check the server journal for details. |
|
| `502 Tinyauth verification error` | Tinyauth returned an unexpected status/body or identity headers were too large. Verify `/api/auth/nginx`, proxy header injection and the installed Tinyauth version; check the server journal for details. |
|
||||||
| `504 Tinyauth timeout` | The Tinyauth verification deadline was reached. Check service load/latency and `connect_timeout_seconds` / `timeout_seconds` (Vhost Authentication tab). |
|
| `504 Tinyauth timeout` | The Tinyauth verification deadline was reached. Check service load/latency and `connect_timeout_seconds` / `timeout_seconds` (Vhost Authentication tab). |
|
||||||
|
| `502 <provider> connection error` / `502 <provider> verification error` / `504 <provider> timeout` | All delegated providers use the same `error_auth_provider.html` template. The name identifies the failing provider; the status identifies network, response-validation, or timeout failure. Check the internal URL, DNS/TLS, allowlist, deadline and RustProxy journal; origin access stays denied. |
|
||||||
|
| Authelia redirects repeatedly | Check `login_url` HTTPS origin, `session.cookies` domains/`authelia_url` mapping, real proxy forwarding headers and original application host. Unrelated registered domains require matching per-domain cookie/portal settings. |
|
||||||
|
| authentik redirects repeatedly or `/outpost.goauthentik.io` is 404 | Check its proxy provider is **Forward auth (single application)** for the exact external application URL, assigned to the correct outpost. Use the outpost internal origin in the Vhost; RustProxy reserves this path on each protected host. Ensure the outpost can reach authentik and browser accepts HTTPS cookies. |
|
||||||
| WebSocket handshake fails or disconnects | Vhost WebSockets flag, backend Upgrade support, TLS/frontend protocol path, WebSocket idle limits and auth redirect. |
|
| WebSocket handshake fails or disconnects | Vhost WebSockets flag, backend Upgrade support, TLS/frontend protocol path, WebSocket idle limits and auth redirect. |
|
||||||
| `403` / `405` / `429` | Applicable IP/ACL/country policy, HTTP methods, rate limit, GeoIP availability, Basic/Tinyauth permissions. |
|
| `403` / `405` / `429` | Applicable IP/ACL/country policy, HTTP methods, rate limit, GeoIP availability, Basic/Tinyauth permissions. |
|
||||||
| Cache always BYPASS or MISS | Global + Vhost enablement, cookies/login, origin `Cache-Control`, `Set-Cookie`, `Vary`, TTL, size and exclusion rules. |
|
| Cache always BYPASS or MISS | Global + Vhost enablement, cookies/login, origin `Cache-Control`, `Set-Cookie`, `Vary`, TTL, size and exclusion rules. |
|
||||||
|
|||||||
@@ -6868,9 +6868,9 @@
|
|||||||
},
|
},
|
||||||
"timepicker": {},
|
"timepicker": {},
|
||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
"title": "RustProxy 0.34.14 - Operations",
|
"title": "RustProxy 0.34.15 - Operations",
|
||||||
"uid": "rustproxy-0170",
|
"uid": "rustproxy-0170",
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"weekStart": "",
|
"weekStart": "",
|
||||||
"description": "RustProxy 0.34.14 Prometheus operations dashboard: traffic and errors, vhosts and backends, Docker discovery, cache capacity and pressure, process and system diagnostics, HTTP/2, HTTP/3, TLS, WebSockets, Layer-4 streams, log sinks, Web Server and PHP-FPM. Some metrics are optional depending on exporter collector settings and enabled services."
|
"description": "RustProxy 0.34.15 Prometheus operations dashboard: traffic and errors, vhosts and backends, Docker discovery, cache capacity and pressure, process and system diagnostics, HTTP/2, HTTP/3, TLS, WebSockets, Layer-4 streams, log sinks, Web Server and PHP-FPM. Some metrics are optional depending on exporter collector settings and enabled services."
|
||||||
}
|
}
|
||||||
+7
-1
@@ -643,7 +643,6 @@
|
|||||||
"certSelectHint": "Select an imported certificate. Uploads and rotation are in the Certificates tab. Assignments are validated against vhost names.",
|
"certSelectHint": "Select an imported certificate. Uploads and rotation are in the Certificates tab. Assignments are validated against vhost names.",
|
||||||
"vhTinyauth": "Authentication",
|
"vhTinyauth": "Authentication",
|
||||||
"vhTabAuth": "Authentication",
|
"vhTabAuth": "Authentication",
|
||||||
"taEnable": "Enable Tinyauth protection",
|
|
||||||
"taUrl": "Tinyauth URL / verification endpoint",
|
"taUrl": "Tinyauth URL / verification endpoint",
|
||||||
"taLogin": "Public login URL",
|
"taLogin": "Public login URL",
|
||||||
"taRedirectForbidden": "Accept same-origin provider redirects for 403",
|
"taRedirectForbidden": "Accept same-origin provider redirects for 403",
|
||||||
@@ -861,6 +860,13 @@
|
|||||||
"authMode": "Authentication",
|
"authMode": "Authentication",
|
||||||
"authNone": "None",
|
"authNone": "None",
|
||||||
"authTinyauth": "Tinyauth",
|
"authTinyauth": "Tinyauth",
|
||||||
|
"authAuthelia": "Authelia",
|
||||||
|
"authAuthentik": "authentik",
|
||||||
|
"forwardAuthUrl": "Forward-auth service URL (internal)",
|
||||||
|
"forwardAuthLogin": "Authelia public login URL (HTTPS)",
|
||||||
|
"forwardAuthAdvanced": "Advanced provider settings",
|
||||||
|
"autheliaHelp": "Calls /api/authz/forward-auth. Select an internal verifier origin and a trusted public login URL. Protected apps can be on separate domains if Authelia is configured with matching session cookies and authentication URLs.",
|
||||||
|
"authentikHelp": "Calls /outpost.goauthentik.io/auth/nginx; RustProxy forwards the /outpost.goauthentik.io path to the same outpost, without requiring separate Custom Locations. Use an authentik Forward auth (single application) provider for each protected app, including apps on different domains.",
|
||||||
"authBasic": "HTTP Basic Auth",
|
"authBasic": "HTTP Basic Auth",
|
||||||
"basicRealm": "Realm (login prompt)",
|
"basicRealm": "Realm (login prompt)",
|
||||||
"basicRequireTls": "Require HTTPS for Basic Auth",
|
"basicRequireTls": "Require HTTPS for Basic Auth",
|
||||||
|
|||||||
+7
-1
@@ -643,7 +643,6 @@
|
|||||||
"certSelectHint": "Wybierz zaimportowany certyfikat. Import i rotacja są w zakładce Certyfikaty. Przypisania są sprawdzane względem nazw vhosta.",
|
"certSelectHint": "Wybierz zaimportowany certyfikat. Import i rotacja są w zakładce Certyfikaty. Przypisania są sprawdzane względem nazw vhosta.",
|
||||||
"vhTinyauth": "Uwierzytelnianie",
|
"vhTinyauth": "Uwierzytelnianie",
|
||||||
"vhTabAuth": "Uwierzytelnianie",
|
"vhTabAuth": "Uwierzytelnianie",
|
||||||
"taEnable": "Włącz ochronę Tinyauth",
|
|
||||||
"taUrl": "URL Tinyauth / punkt weryfikacji",
|
"taUrl": "URL Tinyauth / punkt weryfikacji",
|
||||||
"taLogin": "Publiczny URL logowania",
|
"taLogin": "Publiczny URL logowania",
|
||||||
"taRedirectForbidden": "Akceptuj przekierowanie Tinyauth dla 403 (ten sam origin)",
|
"taRedirectForbidden": "Akceptuj przekierowanie Tinyauth dla 403 (ten sam origin)",
|
||||||
@@ -861,6 +860,13 @@
|
|||||||
"authMode": "Uwierzytelnianie",
|
"authMode": "Uwierzytelnianie",
|
||||||
"authNone": "Brak",
|
"authNone": "Brak",
|
||||||
"authTinyauth": "Tinyauth",
|
"authTinyauth": "Tinyauth",
|
||||||
|
"authAuthelia": "Authelia",
|
||||||
|
"authAuthentik": "authentik",
|
||||||
|
"forwardAuthUrl": "Adres usługi autoryzacji (wewnętrzny)",
|
||||||
|
"forwardAuthLogin": "Publiczny adres logowania Authelia (HTTPS)",
|
||||||
|
"forwardAuthAdvanced": "Zaawansowane ustawienia dostawcy",
|
||||||
|
"autheliaHelp": "Weryfikacja przez /api/authz/forward-auth. Podaj wewnętrzny adres Authelii i publiczny adres logowania HTTPS. Dla różnych domen skonfiguruj odpowiednie cookies i adresy portali w Authelii.",
|
||||||
|
"authentikHelp": "Weryfikacja przez /outpost.goauthentik.io/auth/nginx. RustProxy automatycznie przekazuje ścieżkę /outpost.goauthentik.io do outpostu, bez dodatkowych lokalizacji. Wybierz Forward auth (single application) dla każdej aplikacji, również na innych domenach.",
|
||||||
"authBasic": "HTTP Basic Auth",
|
"authBasic": "HTTP Basic Auth",
|
||||||
"basicRealm": "Realm (nazwa logowania)",
|
"basicRealm": "Realm (nazwa logowania)",
|
||||||
"basicRequireTls": "Wymagaj HTTPS dla Basic Auth",
|
"basicRequireTls": "Wymagaj HTTPS dla Basic Auth",
|
||||||
|
|||||||
+27
-1
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.0.3",
|
"openapi": "3.0.3",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "RustProxy Administration API",
|
"title": "RustProxy Administration API",
|
||||||
"version": "0.34.14",
|
"version": "0.34.15",
|
||||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
@@ -439,6 +439,24 @@
|
|||||||
"id"
|
"id"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ForwardAuthConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Per-Vhost Authelia or authentik verification. Disabled by default. All other settings are optional and use RustProxy defaults; when enabled, url must identify an internal HTTP(S) origin (or expected verifier path). Authelia additionally requires a public HTTPS login_url. External addresses retain normal safe upstream validation, with TLS verification always enabled for HTTPS.",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"enabled": {"type": "boolean", "default": false},
|
||||||
|
"url": {"type": "string", "maxLength": 2048, "description": "Internal service/outpost origin or the provider's verifier endpoint"},
|
||||||
|
"login_url": {"type": "string", "maxLength": 2048, "description": "Required public HTTPS portal URL for Authelia; ignored for authentik"},
|
||||||
|
"allow_private": {"type": "boolean", "default": false},
|
||||||
|
"address_allowlist": {"type": "array", "maxItems": 32, "items": {"type": "string"}},
|
||||||
|
"tls_ca_pem": {"type": "string", "nullable": true},
|
||||||
|
"connect_timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 30, "default": 5},
|
||||||
|
"timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 60, "default": 10},
|
||||||
|
"request_headers": {"type": "array", "maxItems": 32, "items": {"type": "string", "maxLength": 128}, "description": "Only cookie, authorization, user-agent, accept, accept-language and x-api-key are allowed"},
|
||||||
|
"response_headers": {"type": "array", "maxItems": 32, "items": {"type": "string", "maxLength": 128}, "description": "Allowed Remote-*, X-Auth-Request-* and X-Authentik-* identity headers; total injected identity data capped at 8 KiB"},
|
||||||
|
"redirect_forbidden": {"type": "boolean", "default": false, "description": "Authelia only: accept trusted portal Location on explicit 403 if provided"}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Vhost": {
|
"Vhost": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -534,6 +552,14 @@
|
|||||||
"properties": {},
|
"properties": {},
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
},
|
},
|
||||||
|
"authelia": {
|
||||||
|
"$ref": "#/components/schemas/ForwardAuthConfig",
|
||||||
|
"description": "Authelia ForwardAuth. Requires a public HTTPS login_url and /api/authz/forward-auth verifier. Disabled by default; mutually exclusive with basic_auth, tinyauth and authentik."
|
||||||
|
},
|
||||||
|
"authentik": {
|
||||||
|
"$ref": "#/components/schemas/ForwardAuthConfig",
|
||||||
|
"description": "authentik single-application forward auth. The URL is the internal outpost origin; RustProxy proxies its reserved path on the protected host. Disabled by default; mutually exclusive with basic_auth, tinyauth and authelia."
|
||||||
|
},
|
||||||
"cache": {
|
"cache": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ fn status_payload(state: &App) -> Value {
|
|||||||
"security_headers",
|
"security_headers",
|
||||||
"http3",
|
"http3",
|
||||||
"tinyauth",
|
"tinyauth",
|
||||||
|
"authelia_forward_auth",
|
||||||
|
"authentik_forward_auth",
|
||||||
"trusted_forwarding",
|
"trusted_forwarding",
|
||||||
"named_listeners",
|
"named_listeners",
|
||||||
"stream_proxy",
|
"stream_proxy",
|
||||||
|
|||||||
@@ -500,6 +500,16 @@ main {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-provider-options {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 7px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-provider-options>.vh-check {
|
||||||
|
flex: 1 1 250px;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-advanced {
|
.auth-advanced {
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
|
|||||||
+4
-3
@@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>502 - Tinyauth connection error - RustProxy</title>
|
<title>{{STATUS}} - {{PROVIDER}} {{ERROR}} - RustProxy</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 12vh auto;
|
margin: 12vh auto;
|
||||||
@@ -29,8 +29,9 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<h1>502 - Tinyauth connection error</h1>
|
<h1>{{STATUS}} - {{PROVIDER}} {{ERROR}}</h1>
|
||||||
<p>Unable to connect to the Tinyauth authentication service.</p>
|
<p>{{MESSAGE}}</p>
|
||||||
|
<p>Access to the protected application is temporarily unavailable.</p>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<title>504 - Tinyauth timeout - RustProxy</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 12vh auto;
|
|
||||||
padding: 24px;
|
|
||||||
max-width: 620px;
|
|
||||||
background: #131615;
|
|
||||||
color: #e5e9e6;
|
|
||||||
font: 15px/1.6 system-ui, sans-serif
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
margin: 0 0 8px
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
color: #b9c3bc
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<h1>504 - Tinyauth timeout</h1>
|
|
||||||
<p>The Tinyauth authentication service did not respond in time.</p>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<title>502 - Tinyauth verification error - RustProxy</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 12vh auto;
|
|
||||||
padding: 24px;
|
|
||||||
max-width: 620px;
|
|
||||||
background: #131615;
|
|
||||||
color: #e5e9e6;
|
|
||||||
font: 15px/1.6 system-ui, sans-serif
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 24px;
|
|
||||||
margin: 0 0 8px
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
color: #b9c3bc
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<h1>502 - Tinyauth verification error</h1>
|
|
||||||
<p>The Tinyauth authentication service returned an invalid response.</p>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -33,6 +33,18 @@ function hostTagEditor(parent, hosts = [], onUpdate = () => { }) {
|
|||||||
return () => { if (input.value.trim()) consume(input.value); if (!values.length) throw new Error(t("vhHostRequired")); return [...values]; };
|
return () => { if (input.value.trim()) consume(input.value); if (!values.length) throw new Error(t("vhHostRequired")); return [...values]; };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readForwardProvider(id) {
|
||||||
|
const value = key => str(`${id}-${key}`);
|
||||||
|
return {
|
||||||
|
enabled: str("auth-mode") === id,
|
||||||
|
url: value("url"), login_url: id === "authelia" ? value("login") : "",
|
||||||
|
allow_private: yes(`${id}-private`), redirect_forbidden: yes(`${id}-redirect-forbidden`),
|
||||||
|
connect_timeout_seconds: num(`${id}-connect`), timeout_seconds: num(`${id}-timeout`),
|
||||||
|
request_headers: lines(value("request-headers")), response_headers: lines(value("response-headers")),
|
||||||
|
address_allowlist: lines(value("allowlist")), tls_ca_pem: nullable(`${id}-ca`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function load(value = defaults(), migrationIds = [], show = true) {
|
function load(value = defaults(), migrationIds = [], show = true) {
|
||||||
draftEpoch++; stagedCertificates = []; uploading = false; backendHealthEditorControl = null;
|
draftEpoch++; stagedCertificates = []; uploading = false; backendHealthEditorControl = null;
|
||||||
editing = value.id || null; revision = envelope?.revision ?? null; dirty = false; initialized = true;
|
editing = value.id || null; revision = envelope?.revision ?? null; dirty = false; initialized = true;
|
||||||
@@ -332,19 +344,18 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
|||||||
const syncTlsPolicy = () => { for (const input of [tls12, tls13, tlsCiphers, tlsPqc]) input.disabled = !customTlsPolicy.checked; };
|
const syncTlsPolicy = () => { for (const input of [tls12, tls13, tlsCiphers, tlsPqc]) input.disabled = !customTlsPolicy.checked; };
|
||||||
customTlsPolicy.addEventListener("change", syncTlsPolicy); syncTlsPolicy(); note(g, "vhTlsOverrideHelp"); note(g, "tlsPqcVhostHelp");
|
customTlsPolicy.addEventListener("change", syncTlsPolicy); syncTlsPolicy(); note(g, "vhTlsOverrideHelp"); note(g, "tlsPqcVhostHelp");
|
||||||
g = group("vhTinyauth"); const auth = value.tinyauth || {};
|
g = group("vhTinyauth"); const auth = value.tinyauth || {};
|
||||||
const authMode = field(g, "auth-mode", "authMode", value.basic_auth?.enabled ? "basic" : auth.enabled ? "tinyauth" : "none", "select", { values: [["none", "authNone"], ["tinyauth", "authTinyauth"], ["basic", "authBasic"]] });
|
const authMode = field(g, "auth-mode", "authMode", value.basic_auth?.enabled ? "basic" : auth.enabled ? "tinyauth" : value.authelia?.enabled ? "authelia" : value.authentik?.enabled ? "authentik" : "none", "select", { values: [["none", "authNone"], ["tinyauth", "authTinyauth"], ["authelia", "authAuthelia"], ["authentik", "authAuthentik"], ["basic", "authBasic"]] });
|
||||||
note(g, "authModeHelp");
|
note(g, "authModeHelp");
|
||||||
|
|
||||||
const taBody = el("section", undefined, "vh-wide auth-subcard"), basicBody = el("section", undefined, "vh-wide auth-subcard"); g.append(taBody, basicBody);
|
const taBody = el("section", undefined, "vh-wide auth-subcard"), basicBody = el("section", undefined, "vh-wide auth-subcard"); g.append(taBody, basicBody);
|
||||||
taBody.append(text("h3", "authTinyauth"));
|
taBody.append(text("h3", "authTinyauth"));
|
||||||
const taPrimary = el("div", undefined, "vh-grid"); taBody.append(taPrimary);
|
const taPrimary = el("div", undefined, "vh-grid"); taBody.append(taPrimary);
|
||||||
const authEnable = field(taPrimary, "auth-enabled", "taEnable", auth.enabled, "checkbox");
|
|
||||||
const authUrl = field(taPrimary, "auth-url", "taUrl", auth.url || "", "url", { maxLength: 2048 });
|
const authUrl = field(taPrimary, "auth-url", "taUrl", auth.url || "", "url", { maxLength: 2048 });
|
||||||
const loginUrl = field(taPrimary, "auth-login", "taLogin", auth.login_url || "", "url", { maxLength: 2048 });
|
const loginUrl = field(taPrimary, "auth-login", "taLogin", auth.login_url || "", "url", { maxLength: 2048 });
|
||||||
authUrl.placeholder = "http://10.87.7.99:3000"; loginUrl.placeholder = "https://auth.example.com/login";
|
authUrl.placeholder = "http://10.87.7.99:3000"; loginUrl.placeholder = "https://auth.example.com/login";
|
||||||
field(taPrimary, "auth-private", "vhPrivate", auth.allow_private, "checkbox");
|
const taOptions = el("div", undefined, "vh-wide auth-provider-options"); taPrimary.append(taOptions);
|
||||||
field(taPrimary, "auth-redirect-forbidden", "taRedirectForbidden", auth.redirect_forbidden !== false, "checkbox");
|
field(taOptions, "auth-private", "vhPrivate", auth.allow_private, "checkbox");
|
||||||
authEnable.closest("label").hidden = true;
|
field(taOptions, "auth-redirect-forbidden", "taRedirectForbidden", auth.redirect_forbidden !== false, "checkbox");
|
||||||
note(taBody, "taHelp");
|
note(taBody, "taHelp");
|
||||||
|
|
||||||
const taAdvanced = el("details", undefined, "vh-wide auth-advanced"); taAdvanced.append(text("summary", "taAdvanced"));
|
const taAdvanced = el("details", undefined, "vh-wide auth-advanced"); taAdvanced.append(text("summary", "taAdvanced"));
|
||||||
@@ -356,14 +367,49 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
|||||||
field(taAdvancedGrid, "auth-allowlist", "vhAddressAllowlist", (auth.address_allowlist || []).join("\n"), "textarea", { rows: 2, maxLength: 8192 });
|
field(taAdvancedGrid, "auth-allowlist", "vhAddressAllowlist", (auth.address_allowlist || []).join("\n"), "textarea", { rows: 2, maxLength: 8192 });
|
||||||
field(taAdvancedGrid, "auth-ca", "vhCa", auth.tls_ca_pem || "", "textarea", { rows: 2, maxLength: 65536 });
|
field(taAdvancedGrid, "auth-ca", "vhCa", auth.tls_ca_pem || "", "textarea", { rows: 2, maxLength: 65536 });
|
||||||
|
|
||||||
|
const forwardEditors = {};
|
||||||
|
for (const id of ["authelia", "authentik"]) {
|
||||||
|
const current = value[id] || {}, title = id === "authelia" ? "authAuthelia" : "authAuthentik";
|
||||||
|
const body = el("section", undefined, "vh-wide auth-subcard"); g.append(body);
|
||||||
|
body.append(text("h3", title));
|
||||||
|
const primary = el("div", undefined, "vh-grid"); body.append(primary);
|
||||||
|
const url = field(primary, `${id}-url`, "forwardAuthUrl", current.url || "", "url", { maxLength: 2048 });
|
||||||
|
url.placeholder = id === "authelia" ? "http://authelia:9091" : "http://authentik:9000";
|
||||||
|
let login = null;
|
||||||
|
if (id === "authelia") {
|
||||||
|
login = field(primary, `${id}-login`, "forwardAuthLogin", current.login_url || "", "url", { maxLength: 2048 });
|
||||||
|
login.placeholder = "https://auth.example.com";
|
||||||
|
}
|
||||||
|
const options = el("div", undefined, "vh-wide auth-provider-options"); primary.append(options);
|
||||||
|
field(options, `${id}-private`, "vhPrivate", current.allow_private, "checkbox");
|
||||||
|
field(options, `${id}-redirect-forbidden`, "taRedirectForbidden", current.redirect_forbidden === true, "checkbox");
|
||||||
|
note(body, id === "authelia" ? "autheliaHelp" : "authentikHelp");
|
||||||
|
const advanced = el("details", undefined, "vh-wide auth-advanced"); advanced.append(text("summary", "forwardAuthAdvanced"));
|
||||||
|
const details = el("div", undefined, "vh-grid"); advanced.append(details); body.append(advanced);
|
||||||
|
field(details, `${id}-connect`, "taConnect", current.connect_timeout_seconds ?? 5, "number", { required: true, min: 1, max: 30 });
|
||||||
|
field(details, `${id}-timeout`, "taTimeout", current.timeout_seconds ?? 10, "number", { required: true, min: 1, max: 60 });
|
||||||
|
field(details, `${id}-request-headers`, "taRequestHeaders", (current.request_headers || ["cookie", "authorization", "user-agent", "accept", "accept-language"]).join("\n"), "textarea", { rows: 3, maxLength: 8192 });
|
||||||
|
const autheliaHeaders = ["remote-user", "remote-groups", "remote-name", "remote-email"];
|
||||||
|
const authentikHeaders = ["x-authentik-username", "x-authentik-email", "x-authentik-name", "x-authentik-groups", "x-authentik-uid", "x-authentik-entitlements"];
|
||||||
|
field(details, `${id}-response-headers`, "taResponseHeaders", (current.response_headers || [...autheliaHeaders, ...authentikHeaders]).join("\n"), "textarea", { rows: 4, maxLength: 8192 });
|
||||||
|
field(details, `${id}-allowlist`, "vhAddressAllowlist", (current.address_allowlist || []).join("\n"), "textarea", { rows: 2, maxLength: 8192 });
|
||||||
|
field(details, `${id}-ca`, "vhCa", current.tls_ca_pem || "", "textarea", { rows: 2, maxLength: 65536 });
|
||||||
|
forwardEditors[id] = { body, url, login };
|
||||||
|
}
|
||||||
|
|
||||||
basicBody.append(text("h3", "authBasic"));
|
basicBody.append(text("h3", "authBasic"));
|
||||||
const basicGrid = el("div", undefined, "vh-grid"); basicBody.append(basicGrid);
|
const basicGrid = el("div", undefined, "vh-grid"); basicBody.append(basicGrid);
|
||||||
basicEditor = basicAuthEditor(basicGrid, value.basic_auth || {});
|
basicEditor = basicAuthEditor(basicGrid, value.basic_auth || {});
|
||||||
const syncAuth = () => {
|
const syncAuth = () => {
|
||||||
authEnable.checked = authMode.value === "tinyauth";
|
const tinyauthSelected = authMode.value === "tinyauth";
|
||||||
taBody.hidden = !authEnable.checked;
|
taBody.hidden = !tinyauthSelected;
|
||||||
basicBody.hidden = authMode.value !== "basic";
|
basicBody.hidden = authMode.value !== "basic";
|
||||||
authUrl.required = loginUrl.required = authEnable.checked;
|
authUrl.required = loginUrl.required = tinyauthSelected;
|
||||||
|
for (const [id, editor] of Object.entries(forwardEditors)) {
|
||||||
|
editor.body.hidden = id !== authMode.value;
|
||||||
|
editor.url.required = id === authMode.value;
|
||||||
|
if (editor.login) editor.login.required = id === authMode.value;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
authMode.addEventListener("change", syncAuth); syncAuth();
|
authMode.addEventListener("change", syncAuth); syncAuth();
|
||||||
g = group("vhResponsePolicy"); const server = value.server_header, diagnostics = value.diagnostic_headers;
|
g = group("vhResponsePolicy"); const server = value.server_header, diagnostics = value.diagnostic_headers;
|
||||||
@@ -508,7 +554,9 @@ function collect() {
|
|||||||
content_type_policy: { enabled: yes("content-type-block-enabled"), blocked: lines(str("content-type-block-list")).map(value => value.toLowerCase()) },
|
content_type_policy: { enabled: yes("content-type-block-enabled"), blocked: lines(str("content-type-block-list")).map(value => value.toLowerCase()) },
|
||||||
server_header: str("server-mode") === "inherit" ? null : { mode: str("server-mode"), value: str("server-value") },
|
server_header: str("server-mode") === "inherit" ? null : { mode: str("server-mode"), value: str("server-value") },
|
||||||
diagnostic_headers: yes("diag-inherit") ? null : { request_id: yes("diag-request_id") },
|
diagnostic_headers: yes("diag-inherit") ? null : { request_id: yes("diag-request_id") },
|
||||||
tinyauth: { enabled: yes("auth-enabled"), url: str("auth-url"), login_url: str("auth-login"), allow_private: yes("auth-private"), redirect_forbidden: yes("auth-redirect-forbidden"), connect_timeout_seconds: num("auth-connect"), timeout_seconds: num("auth-timeout"), request_headers: lines(str("auth-request-headers")), response_headers: lines(str("auth-response-headers")), address_allowlist: lines(str("auth-allowlist")), tls_ca_pem: nullable("auth-ca") }, backends: members, routes: paths, acl, headers,
|
tinyauth: { enabled: str("auth-mode") === "tinyauth", url: str("auth-url"), login_url: str("auth-login"), allow_private: yes("auth-private"), redirect_forbidden: yes("auth-redirect-forbidden"), connect_timeout_seconds: num("auth-connect"), timeout_seconds: num("auth-timeout"), request_headers: lines(str("auth-request-headers")), response_headers: lines(str("auth-response-headers")), address_allowlist: lines(str("auth-allowlist")), tls_ca_pem: nullable("auth-ca") },
|
||||||
|
authelia: readForwardProvider("authelia"), authentik: readForwardProvider("authentik"),
|
||||||
|
backends: members, routes: paths, acl, headers,
|
||||||
load_balancing: {
|
load_balancing: {
|
||||||
algorithm: str("algorithm"), slow_start_seconds: num("slow-start"), keepalive: { enabled: yes("keepalive"), idle_timeout_seconds: num("idle-timeout"), max_idle_per_host: num("max-idle"), tcp_keepalive_seconds: num("tcp-keepalive"), pool_recycle_requests: num("pool-recycle-requests"), pool_recycle_seconds: num("pool-recycle-seconds"), http1_max_buffer_bytes: num("http1-max-buffer") },
|
algorithm: str("algorithm"), slow_start_seconds: num("slow-start"), keepalive: { enabled: yes("keepalive"), idle_timeout_seconds: num("idle-timeout"), max_idle_per_host: num("max-idle"), tcp_keepalive_seconds: num("tcp-keepalive"), pool_recycle_requests: num("pool-recycle-requests"), pool_recycle_seconds: num("pool-recycle-seconds"), http1_max_buffer_bytes: num("http1-max-buffer") },
|
||||||
retry: { max_retries: num("retry-max"), retry_connect_errors: yes("retry-connect"), retry_timeouts: yes("retry-timeouts"), retry_statuses: str("retry-statuses").split(/[\s,]+/).filter(Boolean).map(value => Number(value)) },
|
retry: { max_retries: num("retry-max"), retry_connect_errors: yes("retry-connect"), retry_timeouts: yes("retry-timeouts"), retry_statuses: str("retry-statuses").split(/[\s,]+/).filter(Boolean).map(value => Number(value)) },
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// Shared, anchored Vhost popover for backend quick-view and upstream HTTP
|
|
||||||
// protocol observations. Both consume existing live status without extra probes.
|
|
||||||
let activeVhostBackendPopup = null;
|
let activeVhostBackendPopup = null;
|
||||||
function closeVhostBackendPopup(returnFocus = false) {
|
function closeVhostBackendPopup(returnFocus = false) {
|
||||||
const current = activeVhostBackendPopup;
|
const current = activeVhostBackendPopup;
|
||||||
@@ -390,6 +388,8 @@ function render() {
|
|||||||
const policy = el("div", undefined, "vhost-policy-pills");
|
const policy = el("div", undefined, "vhost-policy-pills");
|
||||||
if (v.basic_auth?.enabled) policy.append(el("small", t("authBasic"), "pill"));
|
if (v.basic_auth?.enabled) policy.append(el("small", t("authBasic"), "pill"));
|
||||||
if (v.tinyauth?.enabled) policy.append(el("small", "Tinyauth", "pill"));
|
if (v.tinyauth?.enabled) policy.append(el("small", "Tinyauth", "pill"));
|
||||||
|
if (v.authelia?.enabled) policy.append(el("small", "Authelia", "pill"));
|
||||||
|
if (v.authentik?.enabled) policy.append(el("small", "authentik", "pill"));
|
||||||
if (v.rate_limit?.enabled) policy.append(el("small", t("vhRateEnabled"), "pill"));
|
if (v.rate_limit?.enabled) policy.append(el("small", t("vhRateEnabled"), "pill"));
|
||||||
if (v.acl_profile_id) policy.append(el("small", `${t("vhAclProfile")}: ${v.acl_profile_id}`, "pill"));
|
if (v.acl_profile_id) policy.append(el("small", `${t("vhAclProfile")}: ${v.acl_profile_id}`, "pill"));
|
||||||
if (v.force_ssl) policy.append(el("small", t("vhForceSsl"), "pill"));
|
if (v.force_ssl) policy.append(el("small", t("vhForceSsl"), "pill"));
|
||||||
@@ -573,7 +573,6 @@ form.addEventListener("submit", async event => {
|
|||||||
const value = collect(), next = structuredClone(envelope.config); next.vhosts ||= [];
|
const value = collect(), next = structuredClone(envelope.config); next.vhosts ||= [];
|
||||||
const debugIssues = debugOriginIssues(value.backends); if (debugIssues.length && !(await appConfirm(`${t("vhDebugOriginMismatch")}\n\n${debugIssues.map(member => member.id).join(", ")}`, { danger: true }))) return;
|
const debugIssues = debugOriginIssues(value.backends); if (debugIssues.length && !(await appConfirm(`${t("vhDebugOriginMismatch")}\n\n${debugIssues.map(member => member.id).join(", ")}`, { danger: true }))) return;
|
||||||
const oldVhost = (envelope.config.vhosts || []).find(v => v.id === editing);
|
const oldVhost = (envelope.config.vhosts || []).find(v => v.id === editing);
|
||||||
// A previous acceptance must not silently authorize new hostnames or a different certificate.
|
|
||||||
const certificateScope = item => JSON.stringify({
|
const certificateScope = item => JSON.stringify({
|
||||||
certificate: item?.certificate_id || null,
|
certificate: item?.certificate_id || null,
|
||||||
hosts: [...(item?.frontend?.hosts || [])].map(v => v.toLowerCase().replace(/\.$/, "")).sort(),
|
hosts: [...(item?.frontend?.hosts || [])].map(v => v.toLowerCase().replace(/\.$/, "")).sort(),
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use http::{header, HeaderValue, StatusCode};
|
||||||
|
|
||||||
|
use super::AuthenticationResponse;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum GatewayFailure {
|
||||||
|
Connection,
|
||||||
|
Timeout,
|
||||||
|
Verification,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deny(status: StatusCode) -> Response {
|
||||||
|
let body = format!(
|
||||||
|
"{} {}\n",
|
||||||
|
status.as_u16(),
|
||||||
|
status.canonical_reason().unwrap_or("Error")
|
||||||
|
);
|
||||||
|
let mut response = (status, body).into_response();
|
||||||
|
response.headers_mut().insert(
|
||||||
|
header::CACHE_CONTROL,
|
||||||
|
HeaderValue::from_static("private, no-store"),
|
||||||
|
);
|
||||||
|
response.extensions_mut().insert(AuthenticationResponse);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_html(raw: &str) -> String {
|
||||||
|
raw.replace('&', "&")
|
||||||
|
.replace('<', "<")
|
||||||
|
.replace('>', ">")
|
||||||
|
.replace('"', """)
|
||||||
|
.replace('\'', "'")
|
||||||
|
.replace('{', "{")
|
||||||
|
.replace('}', "}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gateway_failure(provider_name: &str, kind: GatewayFailure) -> Response {
|
||||||
|
let (status, label, message) = match kind {
|
||||||
|
GatewayFailure::Connection => (
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
"connection error",
|
||||||
|
"Unable to connect to the",
|
||||||
|
),
|
||||||
|
GatewayFailure::Timeout => (StatusCode::GATEWAY_TIMEOUT, "timeout", "The"),
|
||||||
|
GatewayFailure::Verification => (StatusCode::BAD_GATEWAY, "verification error", "The"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = escape_html(provider_name);
|
||||||
|
let explanation = match kind {
|
||||||
|
GatewayFailure::Connection => format!("{message} {name} authentication service."),
|
||||||
|
GatewayFailure::Timeout => {
|
||||||
|
format!("{message} {name} authentication service did not respond in time.")
|
||||||
|
}
|
||||||
|
GatewayFailure::Verification => {
|
||||||
|
format!("{message} {name} authentication service returned an invalid response.")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let html = include_str!("../admin/web/error_auth_provider.html")
|
||||||
|
.replace("{{STATUS}}", &status.as_u16().to_string())
|
||||||
|
.replace("{{PROVIDER}}", &name)
|
||||||
|
.replace("{{ERROR}}", label)
|
||||||
|
.replace("{{MESSAGE}}", &explanation);
|
||||||
|
let mut response = (status, html).into_response();
|
||||||
|
response.headers_mut().insert(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static("text/html; charset=utf-8"),
|
||||||
|
);
|
||||||
|
response.headers_mut().insert(
|
||||||
|
header::CACHE_CONTROL,
|
||||||
|
HeaderValue::from_static("private, no-store"),
|
||||||
|
);
|
||||||
|
response.headers_mut().insert(
|
||||||
|
"x-content-type-options",
|
||||||
|
HeaderValue::from_static("nosniff"),
|
||||||
|
);
|
||||||
|
response.extensions_mut().insert(AuthenticationResponse);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use http_body_util::BodyExt;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn one_template_covers_every_provider_and_gateway_failure() {
|
||||||
|
for name in ["Tinyauth", "Authelia", "authentik", "Future Provider"] {
|
||||||
|
for (kind, status, description) in [
|
||||||
|
(
|
||||||
|
GatewayFailure::Connection,
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
"connection error",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
GatewayFailure::Verification,
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
"verification error",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
GatewayFailure::Timeout,
|
||||||
|
StatusCode::GATEWAY_TIMEOUT,
|
||||||
|
"timeout",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let response = gateway_failure(name, kind);
|
||||||
|
assert_eq!(response.status(), status);
|
||||||
|
assert_eq!(
|
||||||
|
response.headers()[header::CONTENT_TYPE],
|
||||||
|
"text/html; charset=utf-8"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
response.headers()[header::CACHE_CONTROL],
|
||||||
|
"private, no-store"
|
||||||
|
);
|
||||||
|
assert_eq!(response.headers()["x-content-type-options"], "nosniff");
|
||||||
|
assert!(response
|
||||||
|
.extensions()
|
||||||
|
.get::<AuthenticationResponse>()
|
||||||
|
.is_some());
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
let html = std::str::from_utf8(&body).unwrap();
|
||||||
|
assert!(html.contains(&format!("{} - {name} {description}", status.as_u16())));
|
||||||
|
assert!(
|
||||||
|
!html.contains("{{")
|
||||||
|
&& !html.contains("<script")
|
||||||
|
&& !html.contains("127.0.0.1")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn provider_name_cannot_inject_html() {
|
||||||
|
let response = gateway_failure("<script>alert(1)</script>", GatewayFailure::Connection);
|
||||||
|
let html = String::from_utf8(
|
||||||
|
response
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.to_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(html.contains("<script>"));
|
||||||
|
assert!(!html.contains("<script>"));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn denied_responses_are_marked_and_not_cached() {
|
||||||
|
let response = deny(StatusCode::UNAUTHORIZED);
|
||||||
|
assert!(response
|
||||||
|
.extensions()
|
||||||
|
.get::<AuthenticationResponse>()
|
||||||
|
.is_some());
|
||||||
|
assert_eq!(
|
||||||
|
response.headers()[header::CACHE_CONTROL],
|
||||||
|
"private, no-store"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
use crate::{authelia, authentik, config::ValidationPolicy, forwarding, tinyauth};
|
||||||
|
use anyhow::{ensure, Result};
|
||||||
|
use axum::{body::Body, response::Response};
|
||||||
|
use http::{header, HeaderMap, HeaderValue, Method, Request};
|
||||||
|
use std::{
|
||||||
|
future::Future,
|
||||||
|
net::SocketAddr,
|
||||||
|
pin::Pin,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
pub type Authorization<'a> =
|
||||||
|
Pin<Box<dyn Future<Output = std::result::Result<Identity, Response>> + Send + 'a>>;
|
||||||
|
pub type Route<'a> = Pin<Box<dyn Future<Output = Response> + Send + 'a>>;
|
||||||
|
|
||||||
|
mod errors;
|
||||||
|
pub use errors::{deny, gateway_failure, GatewayFailure};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Identity {
|
||||||
|
pub(crate) headers: HeaderMap,
|
||||||
|
}
|
||||||
|
impl Identity {
|
||||||
|
pub fn apply(&self, headers: &mut HeaderMap) {
|
||||||
|
for name in self.headers.keys() {
|
||||||
|
headers.remove(name);
|
||||||
|
}
|
||||||
|
for (name, value) in &self.headers {
|
||||||
|
headers.append(name.clone(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Provider: Send + Sync + std::fmt::Debug {
|
||||||
|
fn id(&self) -> &'static str;
|
||||||
|
fn strip_identity(&self, headers: &mut HeaderMap);
|
||||||
|
fn authorize<'a>(
|
||||||
|
&'a self,
|
||||||
|
incoming: &'a HeaderMap,
|
||||||
|
method: &'a Method,
|
||||||
|
context: &'a forwarding::Context,
|
||||||
|
shutdown: &'a CancellationToken,
|
||||||
|
cookies: &'a CookieSink,
|
||||||
|
) -> Authorization<'a>;
|
||||||
|
|
||||||
|
fn reserved_path(&self, _path: &str) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn serve_reserved<'a>(
|
||||||
|
&'a self,
|
||||||
|
_request: Request<Body>,
|
||||||
|
_context: &'a forwarding::Context,
|
||||||
|
_shutdown: &'a CancellationToken,
|
||||||
|
) -> Option<Route<'a>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Compiled(Box<dyn Provider>);
|
||||||
|
impl Compiled {
|
||||||
|
fn new<T: Provider + 'static>(provider: T) -> Self {
|
||||||
|
Self(Box::new(provider))
|
||||||
|
}
|
||||||
|
pub fn id(&self) -> &'static str {
|
||||||
|
self.0.id()
|
||||||
|
}
|
||||||
|
pub fn strip_identity(&self, headers: &mut HeaderMap) {
|
||||||
|
self.0.strip_identity(headers);
|
||||||
|
}
|
||||||
|
pub fn reserved_path(&self, path: &str) -> bool {
|
||||||
|
self.0.reserved_path(path)
|
||||||
|
}
|
||||||
|
pub fn serve_reserved<'a>(
|
||||||
|
&'a self,
|
||||||
|
request: Request<Body>,
|
||||||
|
context: &'a forwarding::Context,
|
||||||
|
shutdown: &'a CancellationToken,
|
||||||
|
) -> Option<Route<'a>> {
|
||||||
|
self.0.serve_reserved(request, context, shutdown)
|
||||||
|
}
|
||||||
|
pub async fn authorize(
|
||||||
|
&self,
|
||||||
|
incoming: &HeaderMap,
|
||||||
|
method: &Method,
|
||||||
|
context: &forwarding::Context,
|
||||||
|
shutdown: &CancellationToken,
|
||||||
|
cookies: &CookieSink,
|
||||||
|
) -> std::result::Result<Identity, Response> {
|
||||||
|
self.0
|
||||||
|
.authorize(incoming, method, context, shutdown, cookies)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Selection<'a> {
|
||||||
|
pub basic_enabled: bool,
|
||||||
|
pub tinyauth: &'a tinyauth::Settings,
|
||||||
|
pub authelia: &'a authelia::Settings,
|
||||||
|
pub authentik: &'a authentik::Settings,
|
||||||
|
}
|
||||||
|
impl Selection<'_> {
|
||||||
|
pub fn validate(&self) -> Result<()> {
|
||||||
|
self.tinyauth.validate()?;
|
||||||
|
self.authelia.validate()?;
|
||||||
|
self.authentik.validate()?;
|
||||||
|
let active = [
|
||||||
|
self.basic_enabled,
|
||||||
|
self.tinyauth.enabled,
|
||||||
|
self.authelia.enabled,
|
||||||
|
self.authentik.enabled,
|
||||||
|
];
|
||||||
|
ensure!(
|
||||||
|
active.iter().filter(|enabled| **enabled).count() <= 1,
|
||||||
|
"choose only one authentication provider (Basic, Tinyauth, Authelia or authentik)"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn compile(
|
||||||
|
&self,
|
||||||
|
policy: &ValidationPolicy,
|
||||||
|
bindings: &[SocketAddr],
|
||||||
|
) -> Result<Option<Compiled>> {
|
||||||
|
self.validate()?;
|
||||||
|
if self.tinyauth.enabled {
|
||||||
|
return Ok(self.tinyauth.compile(policy, bindings)?.map(Compiled::new));
|
||||||
|
}
|
||||||
|
if self.authelia.enabled {
|
||||||
|
return Ok(self.authelia.compile(policy, bindings)?.map(Compiled::new));
|
||||||
|
}
|
||||||
|
if self.authentik.enabled {
|
||||||
|
return Ok(self.authentik.compile(policy, bindings)?.map(Compiled::new));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AuthenticationResponse;
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct CookieSink(Arc<Mutex<Vec<HeaderValue>>>);
|
||||||
|
impl CookieSink {
|
||||||
|
pub(crate) fn collect(&self, headers: &HeaderMap) -> bool {
|
||||||
|
let cookies: Vec<_> = headers
|
||||||
|
.get_all(header::SET_COOKIE)
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
let bytes: usize = cookies.iter().map(|value| value.as_bytes().len()).sum();
|
||||||
|
if cookies.len() > 8 || bytes > 16384 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if !cookies.is_empty() {
|
||||||
|
let mut store = self.0.lock().unwrap_or_else(|poison| poison.into_inner());
|
||||||
|
if store.len() + cookies.len() > 8
|
||||||
|
|| store.iter().map(|c| c.as_bytes().len()).sum::<usize>() + bytes > 16384
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
store.extend(cookies);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
pub fn apply(&self, response: &mut Response) {
|
||||||
|
let mut cookies = self.0.lock().unwrap_or_else(|poison| poison.into_inner());
|
||||||
|
for cookie in cookies.drain(..) {
|
||||||
|
response.headers_mut().append(header::SET_COOKIE, cookie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod testing;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn legacy_vhost_keys_are_still_accepted_and_no_provider_is_enabled_by_default() {
|
||||||
|
let host: crate::vhost::Vhost = serde_json::from_value(serde_json::json!({
|
||||||
|
"id": "test", "frontend": {"hosts": ["app.example.com"], "listener_ids": ["https"]}
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
let selection = Selection {
|
||||||
|
basic_enabled: host.basic_auth.enabled,
|
||||||
|
tinyauth: &host.tinyauth,
|
||||||
|
authelia: &host.authelia,
|
||||||
|
authentik: &host.authentik,
|
||||||
|
};
|
||||||
|
assert!(selection.validate().is_ok());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn rejects_multiple_active_providers_before_compilation() {
|
||||||
|
let mut tinyauth = tinyauth::Settings::default();
|
||||||
|
let mut authelia = authelia::Settings::default();
|
||||||
|
let authentik = authentik::Settings::default();
|
||||||
|
tinyauth.enabled = true;
|
||||||
|
authelia.enabled = true;
|
||||||
|
let selection = Selection {
|
||||||
|
basic_enabled: false,
|
||||||
|
tinyauth: &tinyauth,
|
||||||
|
authelia: &authelia,
|
||||||
|
authentik: &authentik,
|
||||||
|
};
|
||||||
|
assert!(selection.validate().is_err());
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct FutureProvider;
|
||||||
|
impl Provider for FutureProvider {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"future"
|
||||||
|
}
|
||||||
|
fn strip_identity(&self, _headers: &mut HeaderMap) {}
|
||||||
|
fn authorize<'a>(
|
||||||
|
&'a self,
|
||||||
|
_incoming: &'a HeaderMap,
|
||||||
|
_method: &'a Method,
|
||||||
|
_context: &'a forwarding::Context,
|
||||||
|
_shutdown: &'a CancellationToken,
|
||||||
|
_cookies: &'a CookieSink,
|
||||||
|
) -> Authorization<'a> {
|
||||||
|
Box::pin(async { Err(deny(http::StatusCode::UNAUTHORIZED)) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn future_providers_can_use_runtime_interface_without_proxy_branches() {
|
||||||
|
let provider = Compiled::new(FutureProvider);
|
||||||
|
assert_eq!(provider.id(), "future");
|
||||||
|
assert!(!provider.reserved_path("/"));
|
||||||
|
let headers = HeaderMap::new();
|
||||||
|
let response = provider
|
||||||
|
.authorize(
|
||||||
|
&headers,
|
||||||
|
&Method::GET,
|
||||||
|
&testing::context(&headers, "app.example.com"),
|
||||||
|
&CancellationToken::new(),
|
||||||
|
&CookieSink::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(response.status(), http::StatusCode::UNAUTHORIZED);
|
||||||
|
assert!(response
|
||||||
|
.extensions()
|
||||||
|
.get::<AuthenticationResponse>()
|
||||||
|
.is_some());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn cookie_sink_is_request_scoped_and_bounded() {
|
||||||
|
let first = CookieSink::default();
|
||||||
|
let second = CookieSink::default();
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.append(header::SET_COOKIE, "session=ok; Path=/".parse().unwrap());
|
||||||
|
assert!(first.collect(&headers));
|
||||||
|
let mut resp = Response::new(Body::empty());
|
||||||
|
second.apply(&mut resp);
|
||||||
|
assert!(resp.headers().get(header::SET_COOKIE).is_none());
|
||||||
|
first.apply(&mut resp);
|
||||||
|
assert_eq!(resp.headers()[header::SET_COOKIE], "session=ok; Path=/");
|
||||||
|
assert_eq!(first.0.lock().unwrap().len(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
use crate::{config::ValidationPolicy, forwarding};
|
||||||
|
use http::HeaderMap;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
|
pub(crate) fn policy() -> ValidationPolicy {
|
||||||
|
ValidationPolicy {
|
||||||
|
proxy_addr: "127.0.0.1:8080".parse().unwrap(),
|
||||||
|
admin_addr: "127.0.0.1:9090".parse().unwrap(),
|
||||||
|
tls_addr: None,
|
||||||
|
cert_dir: None,
|
||||||
|
data_dir: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) fn context(incoming: &HeaderMap, host: &str) -> forwarding::Context {
|
||||||
|
forwarding::Settings::default()
|
||||||
|
.compile()
|
||||||
|
.unwrap()
|
||||||
|
.context(
|
||||||
|
incoming,
|
||||||
|
"192.0.2.20:34567".parse().unwrap(),
|
||||||
|
"10.0.0.1:443".parse().unwrap(),
|
||||||
|
true,
|
||||||
|
&host.parse().unwrap(),
|
||||||
|
&"/private?redirect=%2Fhome".parse().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
pub(crate) async fn spawn_mock(
|
||||||
|
status: &str,
|
||||||
|
response_headers: &str,
|
||||||
|
) -> (String, tokio::task::JoinHandle<String>) {
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let address = listener.local_addr().unwrap();
|
||||||
|
let response = format!(
|
||||||
|
"HTTP/1.1 {status}\r\nContent-Length: 0\r\nConnection: close\r\n{response_headers}\r\n"
|
||||||
|
);
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let (mut stream, _) = listener.accept().await.unwrap();
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
let mut chunk = [0u8; 4096];
|
||||||
|
loop {
|
||||||
|
let n = stream.read(&mut chunk).await.unwrap();
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
bytes.extend_from_slice(&chunk[..n]);
|
||||||
|
if bytes.windows(4).any(|value| value == b"\r\n\r\n") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream.write_all(response.as_bytes()).await.unwrap();
|
||||||
|
String::from_utf8_lossy(&bytes).into_owned()
|
||||||
|
});
|
||||||
|
(format!("http://{address}"), task)
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
use crate::{
|
||||||
|
auth_provider::{self, CookieSink},
|
||||||
|
config::ValidationPolicy,
|
||||||
|
forward_auth, forwarding,
|
||||||
|
};
|
||||||
|
use anyhow::{ensure, Result};
|
||||||
|
use axum::response::Response;
|
||||||
|
use http::{header, HeaderMap, Method, StatusCode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
net::SocketAddr,
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct Settings(forward_auth::Settings);
|
||||||
|
impl Deref for Settings {
|
||||||
|
type Target = forward_auth::Settings;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DerefMut for Settings {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Settings {
|
||||||
|
pub fn validate(&self) -> Result<()> {
|
||||||
|
self.0.validate::<Adapter>()
|
||||||
|
}
|
||||||
|
pub fn compile(
|
||||||
|
&self,
|
||||||
|
policy: &ValidationPolicy,
|
||||||
|
bindings: &[SocketAddr],
|
||||||
|
) -> Result<Option<Compiled>> {
|
||||||
|
Ok(self.0.compile::<Adapter>(policy, bindings)?.map(Compiled))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Compiled(forward_auth::Compiled<Adapter>);
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Adapter;
|
||||||
|
impl forward_auth::Adapter for Adapter {
|
||||||
|
fn label() -> &'static str {
|
||||||
|
"Authelia"
|
||||||
|
}
|
||||||
|
fn verification_path() -> &'static str {
|
||||||
|
"/api/authz/forward-auth"
|
||||||
|
}
|
||||||
|
fn portal(settings: &forward_auth::Settings) -> Result<Option<Url>> {
|
||||||
|
let portal = forward_auth::http_url(&settings.login_url, "Authelia login")?;
|
||||||
|
ensure!(
|
||||||
|
forward_auth::is_portal_https(&portal),
|
||||||
|
"public Authelia login must use HTTPS"
|
||||||
|
);
|
||||||
|
Ok(Some(portal))
|
||||||
|
}
|
||||||
|
fn denied(
|
||||||
|
status: StatusCode,
|
||||||
|
response_headers: &HeaderMap,
|
||||||
|
_original_url: &str,
|
||||||
|
portal: Option<&Url>,
|
||||||
|
redirect_forbidden: bool,
|
||||||
|
) -> std::result::Result<Response, Response> {
|
||||||
|
if matches!(
|
||||||
|
status,
|
||||||
|
StatusCode::FOUND | StatusCode::SEE_OTHER | StatusCode::UNAUTHORIZED
|
||||||
|
) || (status == StatusCode::FORBIDDEN && redirect_forbidden)
|
||||||
|
{
|
||||||
|
let location = portal.and_then(|url| {
|
||||||
|
forward_auth::login_redirect(response_headers.get(header::LOCATION), url)
|
||||||
|
});
|
||||||
|
if let Some(location) = location {
|
||||||
|
let mut response = forward_auth::fail(if status == StatusCode::SEE_OTHER {
|
||||||
|
StatusCode::SEE_OTHER
|
||||||
|
} else {
|
||||||
|
StatusCode::FOUND
|
||||||
|
});
|
||||||
|
response.headers_mut().insert(header::LOCATION, location);
|
||||||
|
Ok(response)
|
||||||
|
} else if matches!(status, StatusCode::FOUND | StatusCode::SEE_OTHER) {
|
||||||
|
Err(forward_auth::invalid(
|
||||||
|
"Authelia",
|
||||||
|
"Authelia returned an untrusted login redirect",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(forward_auth::fail(status))
|
||||||
|
}
|
||||||
|
} else if status == StatusCode::FORBIDDEN {
|
||||||
|
Ok(forward_auth::fail(status))
|
||||||
|
} else {
|
||||||
|
Err(forward_auth::invalid(
|
||||||
|
"Authelia",
|
||||||
|
"Authelia returned an unexpected verifier status",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl auth_provider::Provider for Compiled {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"authelia"
|
||||||
|
}
|
||||||
|
fn strip_identity(&self, headers: &mut HeaderMap) {
|
||||||
|
self.0.strip_identity(headers);
|
||||||
|
}
|
||||||
|
fn authorize<'a>(
|
||||||
|
&'a self,
|
||||||
|
incoming: &'a HeaderMap,
|
||||||
|
method: &'a Method,
|
||||||
|
context: &'a forwarding::Context,
|
||||||
|
shutdown: &'a CancellationToken,
|
||||||
|
cookies: &'a CookieSink,
|
||||||
|
) -> auth_provider::Authorization<'a> {
|
||||||
|
Box::pin(
|
||||||
|
self.0
|
||||||
|
.authorize(incoming, method, context, shutdown, cookies),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
include!("tests.rs");
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::auth_provider::{Provider, testing::{context, policy, spawn_mock}};
|
||||||
|
use http_body_util::BodyExt;
|
||||||
|
|
||||||
|
fn settings(url: &str) -> Settings {
|
||||||
|
let mut settings = Settings::default();
|
||||||
|
settings.enabled = true;
|
||||||
|
settings.url = url.into();
|
||||||
|
settings.login_url = "https://auth.example.com".into();
|
||||||
|
settings.allow_private = true;
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
fn compile(url: &str) -> Compiled { settings(url).compile(&policy(), &[]).unwrap().unwrap() }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_json_shape_and_provider_identity_are_preserved() {
|
||||||
|
let parsed: Settings = serde_json::from_str(r#"{"enabled":true,"url":"http://127.0.0.1:9300","login_url":"https://auth.example.com","allow_private":true}"#).unwrap();
|
||||||
|
assert!(parsed.enabled);
|
||||||
|
let serialized = serde_json::to_value(&parsed).unwrap();
|
||||||
|
assert_eq!(serialized["url"], "http://127.0.0.1:9300");
|
||||||
|
assert!(serialized.get("0").is_none());
|
||||||
|
assert_eq!(compile("http://127.0.0.1:9300").id(), "authelia");
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn invalid_portal_and_cross_origin_redirects_are_rejected() {
|
||||||
|
let mut s = settings("http://127.0.0.1:9300");
|
||||||
|
s.login_url = "http://auth.example.com".into();
|
||||||
|
assert!(s.validate().is_err());
|
||||||
|
let portal = Url::parse("https://auth.example.com/").unwrap();
|
||||||
|
for bad in ["https://evil.example/", "//evil.example", "https://auth.example.com@evil.example/", "javascript:alert(1)", "https://auth.example.com/#frag"] {
|
||||||
|
assert!(forward_auth::login_redirect(Some(&bad.parse().unwrap()), &portal).is_none(), "{bad}");
|
||||||
|
}
|
||||||
|
assert_eq!(forward_auth::login_redirect(Some(&"/login?rd=1".parse().unwrap()), &portal).unwrap(), "https://auth.example.com/login?rd=1");
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn checks_forward_auth_endpoint_and_returns_only_verified_identity() {
|
||||||
|
let (url, server) = spawn_mock("200 OK", "Remote-User: trusted\r\nRemote-Email: trusted@example.com\r\n").await;
|
||||||
|
let auth = compile(&url);
|
||||||
|
let mut incoming = HeaderMap::new();
|
||||||
|
incoming.insert("remote-user", "forged".parse().unwrap());
|
||||||
|
let identity = auth.authorize(&incoming, &Method::GET, &context(&incoming, "app.example.com"),
|
||||||
|
&CancellationToken::new(), &CookieSink::default()).await.unwrap();
|
||||||
|
let request = server.await.unwrap().to_ascii_lowercase();
|
||||||
|
assert!(request.contains("get /api/authz/forward-auth"));
|
||||||
|
assert!(request.contains("x-forwarded-host: app.example.com"));
|
||||||
|
assert!(request.contains("x-forwarded-uri: /private?redirect=%2fhome"));
|
||||||
|
assert!(!request.contains("remote-user: forged"));
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
identity.apply(&mut headers);
|
||||||
|
assert_eq!(headers["remote-user"], "trusted");
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn redirects_only_to_trusted_portal_and_denies_untrusted_redirects() {
|
||||||
|
let (url, server) = spawn_mock("302 Found", "Location: https://auth.example.com/?rd=home\r\n").await;
|
||||||
|
let auth = compile(&url);
|
||||||
|
let incoming = HeaderMap::new();
|
||||||
|
let response = auth.authorize(&incoming, &Method::GET, &context(&incoming, "app.example.com"),
|
||||||
|
&CancellationToken::new(), &CookieSink::default()).await.unwrap_err();
|
||||||
|
assert_eq!(response.status(), StatusCode::FOUND);
|
||||||
|
assert!(response.headers()[header::LOCATION].to_str().unwrap().starts_with("https://auth.example.com/"));
|
||||||
|
server.await.unwrap();
|
||||||
|
|
||||||
|
let (url, server) = spawn_mock("302 Found", "Location: https://evil.example/\r\n").await;
|
||||||
|
let auth = compile(&url);
|
||||||
|
let response = auth.authorize(&incoming, &Method::GET, &context(&incoming, "app.example.com"),
|
||||||
|
&CancellationToken::new(), &CookieSink::default()).await.unwrap_err();
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||||
|
server.await.unwrap();
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unreachable_provider_fails_closed_with_no_origin_details() {
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let url = format!("http://{}", listener.local_addr().unwrap());
|
||||||
|
drop(listener);
|
||||||
|
let auth = compile(&url);
|
||||||
|
let incoming = HeaderMap::new();
|
||||||
|
let response = auth.authorize(&incoming, &Method::GET, &context(&incoming, "app.example.com"),
|
||||||
|
&CancellationToken::new(), &CookieSink::default()).await.unwrap_err();
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||||
|
assert!(response.extensions().get::<auth_provider::AuthenticationResponse>().is_some());
|
||||||
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
|
assert!(!body.windows(url.len()).any(|chunk| chunk == url.as_bytes()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
use crate::{
|
||||||
|
auth_provider::{self, CookieSink},
|
||||||
|
config::ValidationPolicy,
|
||||||
|
forward_auth, forwarding,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::{body::Body, response::Response};
|
||||||
|
use http::{header, HeaderMap, HeaderValue, Method, Request, StatusCode, Uri};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
net::SocketAddr,
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
const OUTPOST_PREFIX: &str = "/outpost.goauthentik.io";
|
||||||
|
pub fn is_outpost_path(path: &str) -> bool {
|
||||||
|
path == OUTPOST_PREFIX || path.starts_with("/outpost.goauthentik.io/")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct Settings(forward_auth::Settings);
|
||||||
|
impl Deref for Settings {
|
||||||
|
type Target = forward_auth::Settings;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DerefMut for Settings {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Settings {
|
||||||
|
pub fn validate(&self) -> Result<()> {
|
||||||
|
self.0.validate::<Adapter>()
|
||||||
|
}
|
||||||
|
pub fn compile(
|
||||||
|
&self,
|
||||||
|
policy: &ValidationPolicy,
|
||||||
|
bindings: &[SocketAddr],
|
||||||
|
) -> Result<Option<Compiled>> {
|
||||||
|
Ok(self.0.compile::<Adapter>(policy, bindings)?.map(Compiled))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Compiled(forward_auth::Compiled<Adapter>);
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Adapter;
|
||||||
|
impl forward_auth::Adapter for Adapter {
|
||||||
|
fn label() -> &'static str {
|
||||||
|
"authentik"
|
||||||
|
}
|
||||||
|
fn verification_path() -> &'static str {
|
||||||
|
"/outpost.goauthentik.io/auth/nginx"
|
||||||
|
}
|
||||||
|
fn denied(
|
||||||
|
status: StatusCode,
|
||||||
|
_response_headers: &HeaderMap,
|
||||||
|
original_url: &str,
|
||||||
|
_portal: Option<&Url>,
|
||||||
|
_redirect_forbidden: bool,
|
||||||
|
) -> std::result::Result<Response, Response> {
|
||||||
|
if status == StatusCode::UNAUTHORIZED {
|
||||||
|
let mut location = Url::parse(original_url).map_err(|_| {
|
||||||
|
forward_auth::invalid("authentik", "invalid original URL for authentik")
|
||||||
|
})?;
|
||||||
|
location.set_path("/outpost.goauthentik.io/start");
|
||||||
|
location.set_query(None);
|
||||||
|
location.query_pairs_mut().append_pair("rd", original_url);
|
||||||
|
let mut response = forward_auth::fail(StatusCode::FOUND);
|
||||||
|
response.headers_mut().insert(
|
||||||
|
header::LOCATION,
|
||||||
|
HeaderValue::from_str(location.as_str()).map_err(|_| {
|
||||||
|
forward_auth::invalid("authentik", "invalid authentik redirect")
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
Ok(response)
|
||||||
|
} else if status == StatusCode::FORBIDDEN {
|
||||||
|
Ok(forward_auth::fail(status))
|
||||||
|
} else {
|
||||||
|
Err(forward_auth::invalid(
|
||||||
|
"authentik",
|
||||||
|
"authentik returned an unexpected verifier status",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl auth_provider::Provider for Compiled {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"authentik"
|
||||||
|
}
|
||||||
|
fn strip_identity(&self, headers: &mut HeaderMap) {
|
||||||
|
self.0.strip_identity(headers);
|
||||||
|
}
|
||||||
|
fn authorize<'a>(
|
||||||
|
&'a self,
|
||||||
|
incoming: &'a HeaderMap,
|
||||||
|
method: &'a Method,
|
||||||
|
context: &'a forwarding::Context,
|
||||||
|
shutdown: &'a CancellationToken,
|
||||||
|
cookies: &'a CookieSink,
|
||||||
|
) -> auth_provider::Authorization<'a> {
|
||||||
|
Box::pin(
|
||||||
|
self.0
|
||||||
|
.authorize(incoming, method, context, shutdown, cookies),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn reserved_path(&self, path: &str) -> bool {
|
||||||
|
is_outpost_path(path)
|
||||||
|
}
|
||||||
|
fn serve_reserved<'a>(
|
||||||
|
&'a self,
|
||||||
|
request: Request<Body>,
|
||||||
|
context: &'a forwarding::Context,
|
||||||
|
shutdown: &'a CancellationToken,
|
||||||
|
) -> Option<auth_provider::Route<'a>> {
|
||||||
|
if !is_outpost_path(request.uri().path()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Box::pin(self.proxy_outpost(request, context, shutdown)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Compiled {
|
||||||
|
async fn proxy_outpost(
|
||||||
|
&self,
|
||||||
|
incoming: Request<Body>,
|
||||||
|
context: &forwarding::Context,
|
||||||
|
shutdown: &CancellationToken,
|
||||||
|
) -> Response {
|
||||||
|
if !is_outpost_path(incoming.uri().path()) {
|
||||||
|
return forward_auth::fail(StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
if incoming.method() != Method::GET
|
||||||
|
&& incoming.method() != Method::HEAD
|
||||||
|
&& incoming.method() != Method::POST
|
||||||
|
{
|
||||||
|
return forward_auth::fail(StatusCode::METHOD_NOT_ALLOWED);
|
||||||
|
}
|
||||||
|
let (parts, body) = incoming.into_parts();
|
||||||
|
let suffix = parts
|
||||||
|
.uri
|
||||||
|
.path_and_query()
|
||||||
|
.map(|value| value.as_str())
|
||||||
|
.unwrap_or(OUTPOST_PREFIX);
|
||||||
|
let uri: Uri = match format!("{}{}", self.0.backend.request_origin, suffix).parse() {
|
||||||
|
Ok(uri) => uri,
|
||||||
|
Err(_) => return forward_auth::fail(StatusCode::BAD_REQUEST),
|
||||||
|
};
|
||||||
|
let mut outbound = match Request::builder()
|
||||||
|
.method(parts.method.clone())
|
||||||
|
.uri(uri)
|
||||||
|
.body(body)
|
||||||
|
{
|
||||||
|
Ok(request) => request,
|
||||||
|
Err(_) => return forward_auth::fail(StatusCode::BAD_REQUEST),
|
||||||
|
};
|
||||||
|
*outbound.headers_mut() = parts.headers;
|
||||||
|
crate::proxy::headers::strip_hop_by_hop(outbound.headers_mut());
|
||||||
|
crate::proxy::headers::strip_admin_cookie(outbound.headers_mut());
|
||||||
|
self.0.strip_identity(outbound.headers_mut());
|
||||||
|
if forward_auth::auth_context(outbound.headers_mut(), &parts.method, context).is_err() {
|
||||||
|
return forward_auth::fail(StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
let original_host = match Url::parse(&context.original_url) {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(_) => return forward_auth::fail(StatusCode::BAD_REQUEST),
|
||||||
|
};
|
||||||
|
let host = match original_host.port() {
|
||||||
|
Some(port) => format!("{}:{port}", original_host.host_str().unwrap_or("")),
|
||||||
|
None => original_host.host_str().unwrap_or("").to_owned(),
|
||||||
|
};
|
||||||
|
if host.is_empty() {
|
||||||
|
return forward_auth::fail(StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if let Ok(host) = host.parse::<HeaderValue>() {
|
||||||
|
outbound.headers_mut().insert(header::HOST, host);
|
||||||
|
} else {
|
||||||
|
return forward_auth::fail(StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
let checked = tokio::select! {
|
||||||
|
_ = shutdown.cancelled() => return forward_auth::fail(StatusCode::SERVICE_UNAVAILABLE),
|
||||||
|
result = tokio::time::timeout(self.0.timeout, self.0.backend.client.request(outbound)) => result,
|
||||||
|
};
|
||||||
|
let response = match checked {
|
||||||
|
Ok(Ok(response)) => response,
|
||||||
|
Ok(Err(error)) => {
|
||||||
|
tracing::warn!(%error, "authentik outpost connection failed");
|
||||||
|
return auth_provider::gateway_failure(
|
||||||
|
"authentik",
|
||||||
|
auth_provider::GatewayFailure::Connection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
return auth_provider::gateway_failure(
|
||||||
|
"authentik",
|
||||||
|
auth_provider::GatewayFailure::Timeout,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (parts, body) = response.into_parts();
|
||||||
|
let mut response = Response::new(Body::new(body));
|
||||||
|
*response.status_mut() = parts.status;
|
||||||
|
*response.headers_mut() = parts.headers;
|
||||||
|
crate::proxy::headers::strip_hop_by_hop(response.headers_mut());
|
||||||
|
response.headers_mut().insert(
|
||||||
|
header::CACHE_CONTROL,
|
||||||
|
HeaderValue::from_static("private, no-store"),
|
||||||
|
);
|
||||||
|
response
|
||||||
|
.extensions_mut()
|
||||||
|
.insert(auth_provider::AuthenticationResponse);
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
include!("tests.rs");
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::auth_provider::{
|
||||||
|
testing::{context, policy, spawn_mock},
|
||||||
|
Provider,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn settings(url: &str) -> Settings {
|
||||||
|
let mut settings = Settings::default();
|
||||||
|
settings.enabled = true;
|
||||||
|
settings.url = url.into();
|
||||||
|
settings.allow_private = true;
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
fn compile(url: &str) -> Compiled {
|
||||||
|
settings(url).compile(&policy(), &[]).unwrap().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_json_and_provider_path_contract_are_preserved() {
|
||||||
|
let parsed: Settings = serde_json::from_str(
|
||||||
|
r#"{"enabled":true,"url":"http://127.0.0.1:9300","allow_private":true}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(&parsed).unwrap()["url"],
|
||||||
|
"http://127.0.0.1:9300"
|
||||||
|
);
|
||||||
|
let auth = compile("http://127.0.0.1:9300");
|
||||||
|
assert_eq!(auth.id(), "authentik");
|
||||||
|
assert!(auth.reserved_path("/outpost.goauthentik.io"));
|
||||||
|
assert!(auth.reserved_path("/outpost.goauthentik.io/start"));
|
||||||
|
assert!(!auth.reserved_path("/outpost.goauthentik.io-evil"));
|
||||||
|
assert!(!auth.reserved_path("/other/outpost.goauthentik.io"));
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unauthorized_redirect_uses_application_domain_not_portal_domain() {
|
||||||
|
let (url, server) = spawn_mock(
|
||||||
|
"401 Unauthorized",
|
||||||
|
"Set-Cookie: authentik_session=abc; Secure; Path=/; HttpOnly\r\n",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let auth = compile(&url);
|
||||||
|
let incoming = HeaderMap::new();
|
||||||
|
let response = auth
|
||||||
|
.authorize(
|
||||||
|
&incoming,
|
||||||
|
&Method::POST,
|
||||||
|
&context(&incoming, "app.other-domain.example"),
|
||||||
|
&CancellationToken::new(),
|
||||||
|
&CookieSink::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(response.status(), StatusCode::FOUND);
|
||||||
|
let location = Url::parse(response.headers()[header::LOCATION].to_str().unwrap()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
location.origin().ascii_serialization(),
|
||||||
|
"https://app.other-domain.example"
|
||||||
|
);
|
||||||
|
assert_eq!(location.path(), "/outpost.goauthentik.io/start");
|
||||||
|
assert_eq!(
|
||||||
|
location.query_pairs().find(|(k, _)| k == "rd").unwrap().1,
|
||||||
|
"https://app.other-domain.example/private?redirect=%2Fhome"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
response.headers()[header::SET_COOKIE],
|
||||||
|
"authentik_session=abc; Secure; Path=/; HttpOnly"
|
||||||
|
);
|
||||||
|
server.await.unwrap();
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn forbidden_response_never_authorizes_backend() {
|
||||||
|
let (url, server) = spawn_mock("403 Forbidden", "").await;
|
||||||
|
let auth = compile(&url);
|
||||||
|
let incoming = HeaderMap::new();
|
||||||
|
let response = auth
|
||||||
|
.authorize(
|
||||||
|
&incoming,
|
||||||
|
&Method::GET,
|
||||||
|
&context(&incoming, "app.example.com"),
|
||||||
|
&CancellationToken::new(),
|
||||||
|
&CookieSink::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||||
|
server.await.unwrap();
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn verified_headers_and_refresh_cookie_are_forwarded() {
|
||||||
|
let (url, server) = spawn_mock("200 OK", "X-Authentik-Username: allowed\r\nSet-Cookie: authentik_session=renew; Path=/; HttpOnly\r\n").await;
|
||||||
|
let auth = compile(&url);
|
||||||
|
let incoming = HeaderMap::new();
|
||||||
|
let cookies = CookieSink::default();
|
||||||
|
let identity = auth
|
||||||
|
.authorize(
|
||||||
|
&incoming,
|
||||||
|
&Method::GET,
|
||||||
|
&context(&incoming, "app.example.com"),
|
||||||
|
&CancellationToken::new(),
|
||||||
|
&cookies,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
identity.apply(&mut headers);
|
||||||
|
assert_eq!(headers["x-authentik-username"], "allowed");
|
||||||
|
let mut response = Response::new(Body::empty());
|
||||||
|
cookies.apply(&mut response);
|
||||||
|
assert_eq!(
|
||||||
|
response.headers()[header::SET_COOKIE],
|
||||||
|
"authentik_session=renew; Path=/; HttpOnly"
|
||||||
|
);
|
||||||
|
server.await.unwrap();
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn outpost_is_proxied_only_on_reserved_path_with_trusted_host() {
|
||||||
|
let (url, server) = spawn_mock("302 Found", "Location: https://auth.example.com/if/flow/default/\r\nSet-Cookie: authentik=ok; Path=/\r\n").await;
|
||||||
|
let auth = compile(&url);
|
||||||
|
let incoming = HeaderMap::new();
|
||||||
|
let request = Request::builder()
|
||||||
|
.uri("/outpost.goauthentik.io/start?rd=https%3A%2F%2Fapp.example.com")
|
||||||
|
.header("X-Forwarded-Host", "spoofed.example")
|
||||||
|
.header("X-Authentik-Username", "forged")
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap();
|
||||||
|
let response = auth
|
||||||
|
.serve_reserved(
|
||||||
|
request,
|
||||||
|
&context(&incoming, "app.example.com"),
|
||||||
|
&CancellationToken::new(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.await;
|
||||||
|
assert_eq!(response.status(), StatusCode::FOUND);
|
||||||
|
assert_eq!(
|
||||||
|
response.headers()[header::SET_COOKIE],
|
||||||
|
"authentik=ok; Path=/"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
response.headers()[header::CACHE_CONTROL],
|
||||||
|
"private, no-store"
|
||||||
|
);
|
||||||
|
let request = server.await.unwrap().to_ascii_lowercase();
|
||||||
|
assert!(request.starts_with("get /outpost.goauthentik.io/start?rd="));
|
||||||
|
assert!(request.contains("host: app.example.com"));
|
||||||
|
assert!(request.contains("x-forwarded-host: app.example.com"));
|
||||||
|
assert!(!request.contains("spoofed.example"));
|
||||||
|
assert!(!request.contains("x-authentik-username: forged"));
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn auth_check_uses_provider_specific_endpoint() {
|
||||||
|
let (url, server) = spawn_mock("200 OK", "X-Authentik-Username: admin\r\n").await;
|
||||||
|
let auth = compile(&url);
|
||||||
|
let incoming = HeaderMap::new();
|
||||||
|
let identity = auth
|
||||||
|
.authorize(
|
||||||
|
&incoming,
|
||||||
|
&Method::GET,
|
||||||
|
&context(&incoming, "app.example.com"),
|
||||||
|
&CancellationToken::new(),
|
||||||
|
&CookieSink::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
identity.apply(&mut headers);
|
||||||
|
assert_eq!(headers["x-authentik-username"], "admin");
|
||||||
|
assert!(server
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.contains("get /outpost.goauthentik.io/auth/nginx"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -154,7 +154,7 @@ fn denied(status: StatusCode, challenge: Option<&HeaderValue>) -> Response {
|
|||||||
}
|
}
|
||||||
response
|
response
|
||||||
.extensions_mut()
|
.extensions_mut()
|
||||||
.insert(crate::tinyauth::AuthenticationFailure);
|
.insert(crate::auth_provider::AuthenticationResponse);
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
fn credentials(headers: &HeaderMap) -> Option<(String, Zeroizing<String>, HeaderValue)> {
|
fn credentials(headers: &HeaderMap) -> Option<(String, Zeroizing<String>, HeaderValue)> {
|
||||||
|
|||||||
+1
-6
@@ -70,8 +70,6 @@ pub struct Config {
|
|||||||
pub vhosts: Vec<crate::vhost::Vhost>,
|
pub vhosts: Vec<crate::vhost::Vhost>,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub host_policies: Vec<HostPolicy>,
|
pub host_policies: Vec<HostPolicy>,
|
||||||
// Preserve old 0.34.13 and earlier config/backup data until compilation migrates it.
|
|
||||||
// Migration happens in compile_with_active, before validation and persistence.
|
|
||||||
#[serde(
|
#[serde(
|
||||||
default,
|
default,
|
||||||
rename = "user_agent_blocking",
|
rename = "user_agent_blocking",
|
||||||
@@ -85,12 +83,9 @@ pub struct Config {
|
|||||||
#[serde(default, skip_serializing_if = "HeaderRules::is_empty")]
|
#[serde(default, skip_serializing_if = "HeaderRules::is_empty")]
|
||||||
pub headers: HeaderRules,
|
pub headers: HeaderRules,
|
||||||
}
|
}
|
||||||
/// Global HTTP public-listener security. Disabled defaults preserve older configurations.
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct GlobalSecurity {
|
pub struct GlobalSecurity {
|
||||||
// None distinguishes omitted legacy input from an explicitly configured empty policy.
|
|
||||||
// Successful compilation always normalizes this to Some(...).
|
|
||||||
pub user_agent_blocking: Option<crate::user_agent::Policy>,
|
pub user_agent_blocking: Option<crate::user_agent::Policy>,
|
||||||
pub country_access: crate::rate_limit::CountryAccess,
|
pub country_access: crate::rate_limit::CountryAccess,
|
||||||
pub acl: Option<Acl>,
|
pub acl: Option<Acl>,
|
||||||
@@ -308,7 +303,7 @@ pub struct CompiledFrontend {
|
|||||||
pub metrics: Arc<crate::metrics::VhostMetrics>,
|
pub metrics: Arc<crate::metrics::VhostMetrics>,
|
||||||
pub server_header: crate::settings::ServerHeader,
|
pub server_header: crate::settings::ServerHeader,
|
||||||
pub diagnostic_headers: crate::settings::DiagnosticHeaders,
|
pub diagnostic_headers: crate::settings::DiagnosticHeaders,
|
||||||
pub tinyauth: Option<crate::tinyauth::Compiled>,
|
pub auth_provider: Option<crate::auth_provider::Compiled>,
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub bindings: Vec<crate::vhost::Listener>,
|
pub bindings: Vec<crate::vhost::Listener>,
|
||||||
pub acl_profile_id: Option<String>,
|
pub acl_profile_id: Option<String>,
|
||||||
|
|||||||
@@ -216,7 +216,13 @@ fn compile_routing(
|
|||||||
.diagnostic_headers
|
.diagnostic_headers
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| config.diagnostic_headers.clone()),
|
.unwrap_or_else(|| config.diagnostic_headers.clone()),
|
||||||
tinyauth: host.tinyauth.compile(&desired_policy, &guarded_bindings)?,
|
auth_provider: crate::auth_provider::Selection {
|
||||||
|
basic_enabled: host.basic_auth.enabled,
|
||||||
|
tinyauth: &host.tinyauth,
|
||||||
|
authelia: &host.authelia,
|
||||||
|
authentik: &host.authentik,
|
||||||
|
}
|
||||||
|
.compile(&desired_policy, &guarded_bindings)?,
|
||||||
id: host.id.clone(),
|
id: host.id.clone(),
|
||||||
bindings: host_bindings,
|
bindings: host_bindings,
|
||||||
acl_profile_id: host.acl_profile_id.clone(),
|
acl_profile_id: host.acl_profile_id.clone(),
|
||||||
|
|||||||
@@ -28,6 +28,43 @@ mod tests {
|
|||||||
.is_none());
|
.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn optional_forward_auth_providers_preserve_legacy_vhost_configuration() {
|
||||||
|
let config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||||
|
let encoded = serde_json::to_value(&config.vhosts[0]).unwrap();
|
||||||
|
let mut legacy = encoded.as_object().unwrap().clone();
|
||||||
|
legacy.remove("authelia");
|
||||||
|
legacy.remove("authentik");
|
||||||
|
let restored: crate::vhost::Vhost = serde_json::from_value(legacy.into()).unwrap();
|
||||||
|
assert!(!restored.authelia.enabled);
|
||||||
|
assert!(!restored.authentik.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forward_auth_selection_must_be_mutually_exclusive() {
|
||||||
|
for (left, right) in [
|
||||||
|
("tinyauth", "authelia"),
|
||||||
|
("authelia", "authentik"),
|
||||||
|
("authentik", "basic"),
|
||||||
|
] {
|
||||||
|
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||||
|
let host = &mut config.vhosts[0];
|
||||||
|
for enabled in [left, right] {
|
||||||
|
match enabled {
|
||||||
|
"tinyauth" => host.tinyauth.enabled = true,
|
||||||
|
"authelia" => host.authelia.enabled = true,
|
||||||
|
"authentik" => host.authentik.enabled = true,
|
||||||
|
"basic" => host.basic_auth.enabled = true,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
host.validate().is_err(),
|
||||||
|
"combination {left} and {right} must be refused"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_http2_settings_use_release_defaults() {
|
fn missing_http2_settings_use_release_defaults() {
|
||||||
let config: Config = serde_json::from_value(serde_json::json!({})).unwrap();
|
let config: Config = serde_json::from_value(serde_json::json!({})).unwrap();
|
||||||
|
|||||||
@@ -0,0 +1,419 @@
|
|||||||
|
use crate::{
|
||||||
|
auth_provider::{self, CookieSink, GatewayFailure, Identity},
|
||||||
|
config::{Backend, CompiledBackend, UpstreamProtocol, ValidationPolicy},
|
||||||
|
forwarding,
|
||||||
|
};
|
||||||
|
use anyhow::{ensure, Context as _, Result};
|
||||||
|
use axum::{body::Body, response::Response};
|
||||||
|
use http::{header, HeaderMap, HeaderName, HeaderValue, Method, Request, StatusCode, Uri};
|
||||||
|
use http_body_util::{BodyExt, Limited};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{marker::PhantomData, net::SocketAddr, sync::Arc, time::Duration};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub trait Adapter: Send + Sync + std::fmt::Debug + 'static {
|
||||||
|
fn label() -> &'static str;
|
||||||
|
fn verification_path() -> &'static str;
|
||||||
|
fn portal(_settings: &Settings) -> Result<Option<Url>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
fn denied(
|
||||||
|
status: StatusCode,
|
||||||
|
response_headers: &HeaderMap,
|
||||||
|
original_url: &str,
|
||||||
|
portal: Option<&Url>,
|
||||||
|
redirect_forbidden: bool,
|
||||||
|
) -> std::result::Result<Response, Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub url: String,
|
||||||
|
pub login_url: String,
|
||||||
|
pub allow_private: bool,
|
||||||
|
pub address_allowlist: Vec<String>,
|
||||||
|
pub tls_ca_pem: Option<String>,
|
||||||
|
pub connect_timeout_seconds: u64,
|
||||||
|
pub timeout_seconds: u64,
|
||||||
|
pub request_headers: Vec<String>,
|
||||||
|
pub response_headers: Vec<String>,
|
||||||
|
pub redirect_forbidden: bool,
|
||||||
|
}
|
||||||
|
impl Default for Settings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
url: String::new(),
|
||||||
|
login_url: String::new(),
|
||||||
|
allow_private: false,
|
||||||
|
address_allowlist: vec![],
|
||||||
|
tls_ca_pem: None,
|
||||||
|
connect_timeout_seconds: 5,
|
||||||
|
timeout_seconds: 10,
|
||||||
|
request_headers: vec![
|
||||||
|
"cookie".into(),
|
||||||
|
"authorization".into(),
|
||||||
|
"user-agent".into(),
|
||||||
|
"accept".into(),
|
||||||
|
"accept-language".into(),
|
||||||
|
],
|
||||||
|
response_headers: vec![
|
||||||
|
"remote-user".into(),
|
||||||
|
"remote-groups".into(),
|
||||||
|
"remote-name".into(),
|
||||||
|
"remote-email".into(),
|
||||||
|
"x-authentik-username".into(),
|
||||||
|
"x-authentik-email".into(),
|
||||||
|
"x-authentik-name".into(),
|
||||||
|
"x-authentik-groups".into(),
|
||||||
|
"x-authentik-uid".into(),
|
||||||
|
"x-authentik-entitlements".into(),
|
||||||
|
],
|
||||||
|
redirect_forbidden: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Compiled<A: Adapter> {
|
||||||
|
pub(crate) backend: Arc<CompiledBackend>,
|
||||||
|
check_uri: Uri,
|
||||||
|
portal: Option<Url>,
|
||||||
|
pub(crate) timeout: Duration,
|
||||||
|
request_headers: Vec<HeaderName>,
|
||||||
|
response_headers: Vec<HeaderName>,
|
||||||
|
_adapter: PhantomData<A>,
|
||||||
|
redirect_forbidden: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fail(status: StatusCode) -> Response {
|
||||||
|
auth_provider::deny(status)
|
||||||
|
}
|
||||||
|
pub(crate) fn invalid(provider_name: &'static str, message: &str) -> Response {
|
||||||
|
tracing::warn!(provider = provider_name, "{message}");
|
||||||
|
auth_provider::gateway_failure(provider_name, GatewayFailure::Verification)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum CheckError {
|
||||||
|
Connection(anyhow::Error),
|
||||||
|
InvalidResponse(anyhow::Error),
|
||||||
|
}
|
||||||
|
pub(crate) fn http_url(value: &str, label: &str) -> Result<Url> {
|
||||||
|
ensure!(
|
||||||
|
(1..=2048).contains(&value.len()),
|
||||||
|
"{label} URL must be 1..2048 bytes"
|
||||||
|
);
|
||||||
|
let url = Url::parse(value)?;
|
||||||
|
ensure!(
|
||||||
|
matches!(url.scheme(), "http" | "https") && url.host_str().is_some(),
|
||||||
|
"{label} URL must use HTTP(S)"
|
||||||
|
);
|
||||||
|
ensure!(
|
||||||
|
url.username().is_empty() && url.password().is_none() && url.fragment().is_none(),
|
||||||
|
"{label} URL cannot contain credentials or fragments"
|
||||||
|
);
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
fn identity_name(name: &HeaderName) -> bool {
|
||||||
|
matches!(
|
||||||
|
name.as_str(),
|
||||||
|
"remote-user"
|
||||||
|
| "remote-email"
|
||||||
|
| "remote-name"
|
||||||
|
| "remote-groups"
|
||||||
|
| "remote-sub"
|
||||||
|
| "x-auth-request-user"
|
||||||
|
| "x-auth-request-email"
|
||||||
|
| "x-auth-request-groups"
|
||||||
|
| "x-auth-request-name"
|
||||||
|
) || name.as_str().starts_with("x-authentik-")
|
||||||
|
}
|
||||||
|
pub(crate) fn is_portal_https(url: &Url) -> bool {
|
||||||
|
url.scheme() == "https"
|
||||||
|
|| url.host_str().is_some_and(|host| {
|
||||||
|
host == "localhost"
|
||||||
|
|| host
|
||||||
|
.parse::<std::net::IpAddr>()
|
||||||
|
.is_ok_and(|ip| ip.is_loopback())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
impl Settings {
|
||||||
|
pub fn validate<A: Adapter>(&self) -> Result<()> {
|
||||||
|
ensure!(
|
||||||
|
(1..=30).contains(&self.connect_timeout_seconds)
|
||||||
|
&& (1..=60).contains(&self.timeout_seconds)
|
||||||
|
&& self.connect_timeout_seconds <= self.timeout_seconds,
|
||||||
|
"{} requires connect timeout <= total timeout (1..30s / 1..60s)",
|
||||||
|
A::label()
|
||||||
|
);
|
||||||
|
ensure!(
|
||||||
|
self.request_headers.len() <= 32 && self.response_headers.len() <= 32,
|
||||||
|
"{} allows at most 32 headers per direction",
|
||||||
|
A::label()
|
||||||
|
);
|
||||||
|
let mut names = std::collections::HashSet::new();
|
||||||
|
for raw in &self.request_headers {
|
||||||
|
ensure!(raw.len() <= 128, "{} request header too long", A::label());
|
||||||
|
let name: HeaderName = raw.parse()?;
|
||||||
|
ensure!(
|
||||||
|
matches!(
|
||||||
|
name.as_str(),
|
||||||
|
"cookie"
|
||||||
|
| "authorization"
|
||||||
|
| "user-agent"
|
||||||
|
| "accept"
|
||||||
|
| "accept-language"
|
||||||
|
| "x-api-key"
|
||||||
|
),
|
||||||
|
"unsafe {} request header: {name}",
|
||||||
|
A::label()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for raw in &self.response_headers {
|
||||||
|
ensure!(raw.len() <= 128, "{} response header too long", A::label());
|
||||||
|
let name: HeaderName = raw.parse()?;
|
||||||
|
ensure!(
|
||||||
|
identity_name(&name) && names.insert(name),
|
||||||
|
"unsafe or duplicate {} identity header",
|
||||||
|
A::label()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.enabled {
|
||||||
|
let url = http_url(&self.url, A::label())?;
|
||||||
|
ensure!(
|
||||||
|
url.query().is_none()
|
||||||
|
&& (url.path() == "/" || url.path() == A::verification_path()),
|
||||||
|
"{} verifier must be an origin or the documented auth endpoint",
|
||||||
|
A::label()
|
||||||
|
);
|
||||||
|
A::portal(self)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn compile<A: Adapter>(
|
||||||
|
&self,
|
||||||
|
policy: &ValidationPolicy,
|
||||||
|
bindings: &[SocketAddr],
|
||||||
|
) -> Result<Option<Compiled<A>>> {
|
||||||
|
self.validate::<A>()?;
|
||||||
|
if !self.enabled {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let mut endpoint = http_url(&self.url, A::label())?;
|
||||||
|
endpoint.set_path(A::verification_path());
|
||||||
|
let origin = endpoint.origin().ascii_serialization();
|
||||||
|
let definition = Backend {
|
||||||
|
id: A::label().to_lowercase(),
|
||||||
|
url: origin,
|
||||||
|
allow_private: self.allow_private,
|
||||||
|
accept_local_listener_risk: false,
|
||||||
|
preserve_host: false,
|
||||||
|
protocol: UpstreamProtocol::Http1,
|
||||||
|
address_allowlist: self.address_allowlist.clone(),
|
||||||
|
tls_ca_pem: self.tls_ca_pem.clone(),
|
||||||
|
tls_server_name: None,
|
||||||
|
tls_verify: true,
|
||||||
|
};
|
||||||
|
let keepalive = crate::balance::Keepalive::default();
|
||||||
|
let mut backend =
|
||||||
|
crate::config::compile_backend(&definition, policy, bindings, &keepalive)?;
|
||||||
|
backend.client = crate::upstream::client_with_timeout(
|
||||||
|
&definition,
|
||||||
|
backend.address_policy.clone(),
|
||||||
|
false,
|
||||||
|
&keepalive,
|
||||||
|
Duration::from_secs(self.connect_timeout_seconds),
|
||||||
|
)?;
|
||||||
|
Ok(Some(Compiled {
|
||||||
|
backend: Arc::new(backend),
|
||||||
|
check_uri: endpoint.as_str().parse()?,
|
||||||
|
portal: A::portal(self)?,
|
||||||
|
timeout: Duration::from_secs(self.timeout_seconds),
|
||||||
|
request_headers: self
|
||||||
|
.request_headers
|
||||||
|
.iter()
|
||||||
|
.map(|value| value.parse())
|
||||||
|
.collect::<std::result::Result<_, _>>()?,
|
||||||
|
response_headers: self
|
||||||
|
.response_headers
|
||||||
|
.iter()
|
||||||
|
.map(|value| value.parse())
|
||||||
|
.collect::<std::result::Result<_, _>>()?,
|
||||||
|
redirect_forbidden: self.redirect_forbidden,
|
||||||
|
_adapter: PhantomData,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) fn auth_context(
|
||||||
|
headers: &mut HeaderMap,
|
||||||
|
method: &Method,
|
||||||
|
context: &forwarding::Context,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Never trust inbound original metadata, including markers from another auth module.
|
||||||
|
for name in headers
|
||||||
|
.keys()
|
||||||
|
.filter(|name| name.as_str().starts_with("x-original-"))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
{
|
||||||
|
headers.remove(name);
|
||||||
|
}
|
||||||
|
context.apply(headers);
|
||||||
|
headers.insert("x-original-url", context.original_url.parse()?);
|
||||||
|
headers.insert("x-original-method", method.as_str().parse()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub(crate) fn login_redirect(raw: Option<&HeaderValue>, portal: &Url) -> Option<HeaderValue> {
|
||||||
|
let raw = raw?.to_str().ok()?;
|
||||||
|
if raw.len() > 8192 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let url = if raw.starts_with('/') && !raw.starts_with("//") {
|
||||||
|
portal.join(raw).ok()?
|
||||||
|
} else {
|
||||||
|
Url::parse(raw).ok()?
|
||||||
|
};
|
||||||
|
if url.origin() != portal.origin()
|
||||||
|
|| !url.username().is_empty()
|
||||||
|
|| url.password().is_some()
|
||||||
|
|| url.fragment().is_some()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
url.as_str().parse().ok()
|
||||||
|
}
|
||||||
|
impl<A: Adapter> Compiled<A> {
|
||||||
|
pub fn strip_identity(&self, headers: &mut HeaderMap) {
|
||||||
|
for name in headers
|
||||||
|
.keys()
|
||||||
|
.filter(|name| identity_name(name))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
{
|
||||||
|
headers.remove(name);
|
||||||
|
}
|
||||||
|
for name in &self.response_headers {
|
||||||
|
headers.remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub async fn authorize(
|
||||||
|
&self,
|
||||||
|
incoming: &HeaderMap,
|
||||||
|
method: &Method,
|
||||||
|
context: &forwarding::Context,
|
||||||
|
shutdown: &tokio_util::sync::CancellationToken,
|
||||||
|
cookies: &CookieSink,
|
||||||
|
) -> std::result::Result<Identity, Response> {
|
||||||
|
let check = tokio::select! {
|
||||||
|
_ = shutdown.cancelled() => return Err(fail(StatusCode::SERVICE_UNAVAILABLE)),
|
||||||
|
result = tokio::time::timeout(self.timeout, self.check(incoming, method, context)) => result,
|
||||||
|
};
|
||||||
|
let (status, response_headers) = match check {
|
||||||
|
Ok(Ok(check)) => check,
|
||||||
|
Ok(Err(CheckError::Connection(error))) => {
|
||||||
|
tracing::warn!(provider = A::label(), %error, "forward-auth connection failed");
|
||||||
|
return Err(auth_provider::gateway_failure(
|
||||||
|
A::label(),
|
||||||
|
GatewayFailure::Connection,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Err(CheckError::InvalidResponse(error))) => {
|
||||||
|
tracing::warn!(provider = A::label(), %error, "forward-auth response invalid");
|
||||||
|
return Err(auth_provider::gateway_failure(
|
||||||
|
A::label(),
|
||||||
|
GatewayFailure::Verification,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
tracing::warn!(provider = A::label(), "forward-auth verifier timed out");
|
||||||
|
return Err(auth_provider::gateway_failure(
|
||||||
|
A::label(),
|
||||||
|
GatewayFailure::Timeout,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if status == StatusCode::OK {
|
||||||
|
let mut identity = HeaderMap::new();
|
||||||
|
let mut size = 0usize;
|
||||||
|
for name in &self.response_headers {
|
||||||
|
for value in response_headers.get_all(name) {
|
||||||
|
size += value.as_bytes().len();
|
||||||
|
if size > 8192 {
|
||||||
|
return Err(invalid(
|
||||||
|
A::label(),
|
||||||
|
"forward-auth identity headers too large",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
identity.append(name.clone(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !cookies.collect(&response_headers) {
|
||||||
|
return Err(invalid(
|
||||||
|
A::label(),
|
||||||
|
"forward-auth session headers too large",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Ok(Identity { headers: identity });
|
||||||
|
}
|
||||||
|
let mut response = A::denied(
|
||||||
|
status,
|
||||||
|
&response_headers,
|
||||||
|
&context.original_url,
|
||||||
|
self.portal.as_ref(),
|
||||||
|
self.redirect_forbidden,
|
||||||
|
)?;
|
||||||
|
let session = CookieSink::default();
|
||||||
|
if !session.collect(&response_headers) {
|
||||||
|
return Err(invalid(
|
||||||
|
A::label(),
|
||||||
|
"forward-auth response cookies too large",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
session.apply(&mut response);
|
||||||
|
Err(response)
|
||||||
|
}
|
||||||
|
async fn check(
|
||||||
|
&self,
|
||||||
|
incoming: &HeaderMap,
|
||||||
|
method: &Method,
|
||||||
|
context: &forwarding::Context,
|
||||||
|
) -> std::result::Result<(StatusCode, HeaderMap), CheckError> {
|
||||||
|
let mut request = Request::builder()
|
||||||
|
.method(Method::GET)
|
||||||
|
.uri(self.check_uri.clone())
|
||||||
|
.body(Body::empty())
|
||||||
|
.map_err(|error| CheckError::InvalidResponse(error.into()))?;
|
||||||
|
for name in &self.request_headers {
|
||||||
|
for value in incoming.get_all(name) {
|
||||||
|
request.headers_mut().append(name.clone(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::proxy::headers::strip_hop_by_hop(request.headers_mut());
|
||||||
|
crate::proxy::headers::strip_admin_cookie(request.headers_mut());
|
||||||
|
self.strip_identity(request.headers_mut());
|
||||||
|
auth_context(request.headers_mut(), method, context)
|
||||||
|
.map_err(CheckError::InvalidResponse)?;
|
||||||
|
request
|
||||||
|
.headers_mut()
|
||||||
|
.insert(header::HOST, self.backend.host_header.clone());
|
||||||
|
let response = self
|
||||||
|
.backend
|
||||||
|
.client
|
||||||
|
.request(request)
|
||||||
|
.await
|
||||||
|
.context("forward-auth connection")
|
||||||
|
.map_err(CheckError::Connection)?;
|
||||||
|
let (parts, body) = response.into_parts();
|
||||||
|
Limited::new(body, 16 * 1024)
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.map_err(|error| anyhow::anyhow!("forward-auth verification body: {error}"))
|
||||||
|
.map_err(CheckError::InvalidResponse)?;
|
||||||
|
Ok((parts.status, parts.headers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
include!("tests.rs");
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{authelia, authentik};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn all_forward_auth_adapters_use_generic_gateway_pages() {
|
||||||
|
use crate::auth_provider::testing::{context, policy, spawn_mock};
|
||||||
|
use http_body_util::BodyExt;
|
||||||
|
|
||||||
|
let incoming = HeaderMap::new();
|
||||||
|
for provider in ["Authelia", "authentik"] {
|
||||||
|
let (url, server) = spawn_mock("503 Service Unavailable", "").await;
|
||||||
|
let settings = Settings {
|
||||||
|
enabled: true,
|
||||||
|
url,
|
||||||
|
login_url: "https://auth.example.com".into(),
|
||||||
|
allow_private: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let result = if provider == "Authelia" {
|
||||||
|
settings
|
||||||
|
.compile::<authelia::Adapter>(&policy(), &[])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.authorize(
|
||||||
|
&incoming,
|
||||||
|
&Method::GET,
|
||||||
|
&context(&incoming, "app.example.com"),
|
||||||
|
&tokio_util::sync::CancellationToken::new(),
|
||||||
|
&CookieSink::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
settings
|
||||||
|
.compile::<authentik::Adapter>(&policy(), &[])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.authorize(
|
||||||
|
&incoming,
|
||||||
|
&Method::GET,
|
||||||
|
&context(&incoming, "app.example.com"),
|
||||||
|
&tokio_util::sync::CancellationToken::new(),
|
||||||
|
&CookieSink::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
let response = result.unwrap_err();
|
||||||
|
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||||
|
let html = String::from_utf8(
|
||||||
|
response
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.to_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(html.contains(&format!("{provider} verification error")));
|
||||||
|
server.await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let url = format!("http://{}", listener.local_addr().unwrap());
|
||||||
|
drop(listener);
|
||||||
|
let settings = Settings {
|
||||||
|
enabled: true,
|
||||||
|
url,
|
||||||
|
login_url: "https://auth.example.com".into(),
|
||||||
|
allow_private: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let context = context(&incoming, "app.example.com");
|
||||||
|
let shutdown = tokio_util::sync::CancellationToken::new();
|
||||||
|
let cookies = CookieSink::default();
|
||||||
|
let result = settings
|
||||||
|
.compile::<authelia::Adapter>(&policy(), &[])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.authorize(&incoming, &Method::GET, &context, &shutdown, &cookies)
|
||||||
|
.await;
|
||||||
|
let response = result.unwrap_err();
|
||||||
|
let html = String::from_utf8(
|
||||||
|
response
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.to_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(html.contains("Authelia connection error"));
|
||||||
|
let result = settings
|
||||||
|
.compile::<authentik::Adapter>(&policy(), &[])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.authorize(&incoming, &Method::GET, &context, &shutdown, &cookies)
|
||||||
|
.await;
|
||||||
|
let response = result.unwrap_err();
|
||||||
|
let html = String::from_utf8(
|
||||||
|
response
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.to_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(html.contains("authentik connection error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn verifier_deadline_produces_generic_504_page_for_both_adapters() {
|
||||||
|
use crate::auth_provider::testing::{context, policy};
|
||||||
|
use http_body_util::BodyExt;
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let settings = Settings {
|
||||||
|
enabled: true,
|
||||||
|
url: format!("http://{}", listener.local_addr().unwrap()),
|
||||||
|
login_url: "https://auth.example.com".into(),
|
||||||
|
allow_private: true,
|
||||||
|
connect_timeout_seconds: 1,
|
||||||
|
timeout_seconds: 1,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let incoming = HeaderMap::new();
|
||||||
|
let ctx = context(&incoming, "app.example.com");
|
||||||
|
let shutdown = tokio_util::sync::CancellationToken::new();
|
||||||
|
let cookies = CookieSink::default();
|
||||||
|
let response = settings
|
||||||
|
.compile::<authelia::Adapter>(&policy(), &[])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.authorize(&incoming, &Method::GET, &ctx, &shutdown, &cookies)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT);
|
||||||
|
let html = String::from_utf8(
|
||||||
|
response
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.to_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(html.contains("Authelia timeout"));
|
||||||
|
let response = settings
|
||||||
|
.compile::<authentik::Adapter>(&policy(), &[])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.authorize(&incoming, &Method::GET, &ctx, &shutdown, &cookies)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT);
|
||||||
|
let html = String::from_utf8(
|
||||||
|
response
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.to_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(html.contains("authentik timeout"));
|
||||||
|
drop(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_forward_auth_configs_are_backward_compatible() {
|
||||||
|
for empty in ["{}", "{\"enabled\":false}"] {
|
||||||
|
let settings: Settings = serde_json::from_str(empty).unwrap();
|
||||||
|
assert!(!settings.enabled);
|
||||||
|
assert!(settings.validate::<authelia::Adapter>().is_ok());
|
||||||
|
assert!(settings.validate::<authentik::Adapter>().is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn provider_verification_endpoint_and_header_safety_are_checked() {
|
||||||
|
for provider in ["authelia", "authentik"] {
|
||||||
|
let mut settings = Settings {
|
||||||
|
enabled: true,
|
||||||
|
url: "http://127.0.0.1:9300".into(),
|
||||||
|
login_url: "https://auth.example.com".into(),
|
||||||
|
allow_private: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let validate = |settings: &Settings| -> Result<()> {
|
||||||
|
if provider == "authelia" {
|
||||||
|
settings.validate::<authelia::Adapter>()
|
||||||
|
} else {
|
||||||
|
settings.validate::<authentik::Adapter>()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(validate(&settings).is_ok());
|
||||||
|
for bad in [
|
||||||
|
"file:///etc/passwd",
|
||||||
|
"http://user:password@127.0.0.1:9300",
|
||||||
|
"http://127.0.0.1:9300/other",
|
||||||
|
"http://127.0.0.1:9300?q=1",
|
||||||
|
"https://example/#frag",
|
||||||
|
] {
|
||||||
|
settings.url = bad.into();
|
||||||
|
assert!(validate(&settings).is_err(), "{provider}: {bad}");
|
||||||
|
}
|
||||||
|
settings.url = "http://127.0.0.1:9300".into();
|
||||||
|
settings.request_headers.push("x-forwarded-host".into());
|
||||||
|
assert!(validate(&settings).is_err());
|
||||||
|
settings.request_headers.pop();
|
||||||
|
settings.response_headers.push("authorization".into());
|
||||||
|
assert!(validate(&settings).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn original_request_headers_are_regenerated_and_identity_is_sanitized() {
|
||||||
|
use crate::auth_provider::testing::{context, policy};
|
||||||
|
let settings = Settings {
|
||||||
|
enabled: true,
|
||||||
|
url: "http://127.0.0.1:9300".into(),
|
||||||
|
allow_private: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let compiled = settings
|
||||||
|
.compile::<authentik::Adapter>(&policy(), &[])
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("remote-user", "forged".parse().unwrap());
|
||||||
|
headers.insert("x-authentik-username", "forged".parse().unwrap());
|
||||||
|
headers.insert(
|
||||||
|
"x-original-url",
|
||||||
|
"https://evil.example/private".parse().unwrap(),
|
||||||
|
);
|
||||||
|
headers.insert("x-forwarded-host", "evil.example".parse().unwrap());
|
||||||
|
let ctx = context(&headers, "app.example.com");
|
||||||
|
compiled.strip_identity(&mut headers);
|
||||||
|
auth_context(&mut headers, &Method::POST, &ctx).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
headers["x-original-url"],
|
||||||
|
"https://app.example.com/private?redirect=%2Fhome"
|
||||||
|
);
|
||||||
|
assert_eq!(headers["x-forwarded-host"], "app.example.com");
|
||||||
|
assert_eq!(headers["x-original-method"], "POST");
|
||||||
|
assert!(!headers.contains_key("remote-user"));
|
||||||
|
assert!(!headers.contains_key("x-authentik-username"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,7 +31,11 @@ pub mod stream;
|
|||||||
pub mod system_stats;
|
pub mod system_stats;
|
||||||
pub mod transform;
|
pub mod transform;
|
||||||
|
|
||||||
|
pub mod auth_provider;
|
||||||
|
pub mod authelia;
|
||||||
|
pub mod authentik;
|
||||||
pub mod balance;
|
pub mod balance;
|
||||||
|
pub mod forward_auth;
|
||||||
pub mod forwarding;
|
pub mod forwarding;
|
||||||
pub mod geoip;
|
pub mod geoip;
|
||||||
pub mod http3;
|
pub mod http3;
|
||||||
|
|||||||
@@ -267,7 +267,6 @@ fn finish_headers(
|
|||||||
request_id: &str,
|
request_id: &str,
|
||||||
active: &[crate::vhost::Listener],
|
active: &[crate::vhost::Listener],
|
||||||
) {
|
) {
|
||||||
// Global security headers form a baseline; explicitly set vhost values take precedence.
|
|
||||||
snapshot
|
snapshot
|
||||||
.global_security
|
.global_security
|
||||||
.headers
|
.headers
|
||||||
@@ -283,7 +282,7 @@ fn finish_headers(
|
|||||||
.map(|f| &f.diagnostic_headers)
|
.map(|f| &f.diagnostic_headers)
|
||||||
.unwrap_or(&snapshot.envelope.config.diagnostic_headers)
|
.unwrap_or(&snapshot.envelope.config.diagnostic_headers)
|
||||||
.apply(response.headers_mut(), request_id);
|
.apply(response.headers_mut(), request_id);
|
||||||
if frontend.is_some_and(|f| f.tinyauth.is_some() || f.basic_auth.is_some()) {
|
if frontend.is_some_and(|f| f.auth_provider.is_some() || f.basic_auth.is_some()) {
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
"cache-control",
|
"cache-control",
|
||||||
HeaderValue::from_static("private, no-store"),
|
HeaderValue::from_static("private, no-store"),
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ fn disabled_cache_retains_reason_without_matching_force_rules_or_reparsing_heade
|
|||||||
"admin_control_plane",
|
"admin_control_plane",
|
||||||
"basic_auth",
|
"basic_auth",
|
||||||
"tinyauth",
|
"tinyauth",
|
||||||
|
"authelia",
|
||||||
|
"authentik",
|
||||||
"sticky_cookie",
|
"sticky_cookie",
|
||||||
"request_upgrade",
|
"request_upgrade",
|
||||||
] {
|
] {
|
||||||
|
|||||||
@@ -127,8 +127,8 @@ let policy_reason = if admin_control_plane {
|
|||||||
Some("admin_control_plane")
|
Some("admin_control_plane")
|
||||||
} else if frontend.is_some_and(|f| f.basic_auth.is_some()) {
|
} else if frontend.is_some_and(|f| f.basic_auth.is_some()) {
|
||||||
Some("basic_auth")
|
Some("basic_auth")
|
||||||
} else if frontend.is_some_and(|f| f.tinyauth.is_some()) {
|
} else if let Some(provider) = frontend.and_then(|f| f.auth_provider.as_ref()) {
|
||||||
Some("tinyauth")
|
Some(provider.id())
|
||||||
} else if route
|
} else if route
|
||||||
.pool
|
.pool
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -244,7 +244,7 @@ context.apply_upstream(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_none_or(|frontend| frontend.forward_client_ip),
|
.is_none_or(|frontend| frontend.forward_client_ip),
|
||||||
);
|
);
|
||||||
if let Some(auth) = frontend.and_then(|f| f.tinyauth.as_ref()) {
|
if let Some(auth) = frontend.and_then(|f| f.auth_provider.as_ref()) {
|
||||||
auth.strip_identity(request.headers_mut());
|
auth.strip_identity(request.headers_mut());
|
||||||
}
|
}
|
||||||
if let Some(identity) = identity {
|
if let Some(identity) = identity {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
let transform_settings = route.frontend.as_ref().map(|frontend| &frontend.transform);
|
let transform_settings = route.frontend.as_ref().map(|frontend| &frontend.transform);
|
||||||
let scope = route.frontend.as_ref().map(|frontend| frontend.id.as_str()).unwrap_or("");
|
let scope = route.frontend.as_ref().map(|frontend| frontend.id.as_str()).unwrap_or("");
|
||||||
let enabled = cache_settings.active_for(route.cache)
|
let enabled = cache_settings.active_for(route.cache)
|
||||||
&& frontend.is_none_or(|frontend| frontend.basic_auth.is_none() && frontend.tinyauth.is_none());
|
&& frontend.is_none_or(|frontend| frontend.basic_auth.is_none() && frontend.auth_provider.is_none());
|
||||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||||
exchange.set_cache_enabled(enabled);
|
exchange.set_cache_enabled(enabled);
|
||||||
exchange.set_transform_enabled(transform_settings.is_some_and(|settings| settings.enabled));
|
exchange.set_transform_enabled(transform_settings.is_some_and(|settings| settings.enabled));
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ async fn forward(
|
|||||||
State(state): State<Arc<App>>,
|
State(state): State<Arc<App>>,
|
||||||
Extension(peer): Extension<PeerAddr>,
|
Extension(peer): Extension<PeerAddr>,
|
||||||
Extension(local): Extension<LocalAddr>,
|
Extension(local): Extension<LocalAddr>,
|
||||||
request: Request,
|
mut request: Request,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
forward_request(state, peer, local, request).await
|
let cookies = crate::auth_provider::CookieSink::default();
|
||||||
|
request.extensions_mut().insert(cookies.clone());
|
||||||
|
let mut response = forward_request(state, peer, local, request).await;
|
||||||
|
cookies.apply(&mut response);
|
||||||
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn forward_request(
|
async fn forward_request(
|
||||||
@@ -197,7 +201,7 @@ async fn forward_request(
|
|||||||
tls,
|
tls,
|
||||||
&original_host,
|
&original_host,
|
||||||
request.uri(),
|
request.uri(),
|
||||||
frontend.is_some_and(|frontend| frontend.tinyauth.is_some()),
|
frontend.is_some_and(|frontend| frontend.auth_provider.is_some()),
|
||||||
request
|
request
|
||||||
.extensions()
|
.extensions()
|
||||||
.get::<Arc<crate::forwarding::PeerFormatting>>()
|
.get::<Arc<crate::forwarding::PeerFormatting>>()
|
||||||
@@ -464,6 +468,18 @@ async fn forward_request(
|
|||||||
.filter(|route| route.error_fallback.is_some())
|
.filter(|route| route.error_fallback.is_some())
|
||||||
.map(|_| ErrorFallbackRequest::capture(&request, &snapshot));
|
.map(|_| ErrorFallbackRequest::capture(&request, &snapshot));
|
||||||
let admin_cookie = headers::strip_admin_cookie(request.headers_mut());
|
let admin_cookie = headers::strip_admin_cookie(request.headers_mut());
|
||||||
|
|
||||||
|
if let Some(auth) = frontend.and_then(|frontend| frontend.auth_provider.as_ref()) {
|
||||||
|
if auth.reserved_path(&path) {
|
||||||
|
return match auth.serve_reserved(request, &context, &state.shutdown) {
|
||||||
|
Some(response) => response.await,
|
||||||
|
None => error(
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
"RustProxy: authentication route unavailable\n",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
let basic_identity = if let Some(auth) = frontend.and_then(|f| f.basic_auth.as_ref()) {
|
let basic_identity = if let Some(auth) = frontend.and_then(|f| f.basic_auth.as_ref()) {
|
||||||
match auth
|
match auth
|
||||||
.authorize(
|
.authorize(
|
||||||
@@ -487,14 +503,20 @@ async fn forward_request(
|
|||||||
"RustProxy: WebSocket access is disabled for this host\n",
|
"RustProxy: WebSocket access is disabled for this host\n",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let identity = if let Some(auth) = frontend.and_then(|f| f.tinyauth.as_ref()) {
|
let identity = if let Some(auth) = frontend.and_then(|f| f.auth_provider.as_ref()) {
|
||||||
auth.strip_identity(request.headers_mut());
|
auth.strip_identity(request.headers_mut());
|
||||||
|
let cookies = request
|
||||||
|
.extensions()
|
||||||
|
.get::<crate::auth_provider::CookieSink>()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
match auth
|
match auth
|
||||||
.authorize(
|
.authorize(
|
||||||
request.headers(),
|
request.headers(),
|
||||||
request.method(),
|
request.method(),
|
||||||
&context,
|
&context,
|
||||||
&state.shutdown,
|
&state.shutdown,
|
||||||
|
&cookies,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ fn should_replace_error_page(response: &Response, minimal_welcome: bool) -> bool
|
|||||||
.is_none()
|
.is_none()
|
||||||
&& response
|
&& response
|
||||||
.extensions()
|
.extensions()
|
||||||
.get::<crate::tinyauth::AuthenticationFailure>()
|
.get::<crate::auth_provider::AuthenticationResponse>()
|
||||||
.is_none()
|
.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
|||||||
.is_none()
|
.is_none()
|
||||||
&& response
|
&& response
|
||||||
.extensions()
|
.extensions()
|
||||||
.get::<crate::tinyauth::AuthenticationFailure>()
|
.get::<crate::auth_provider::AuthenticationResponse>()
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
apply_response_rules(
|
apply_response_rules(
|
||||||
@@ -385,7 +385,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
|||||||
.is_none()
|
.is_none()
|
||||||
&& response
|
&& response
|
||||||
.extensions()
|
.extensions()
|
||||||
.get::<crate::tinyauth::AuthenticationFailure>()
|
.get::<crate::auth_provider::AuthenticationResponse>()
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
let outcome = match exchange.cache_status.load(Ordering::Relaxed) {
|
let outcome = match exchange.cache_status.load(Ordering::Relaxed) {
|
||||||
@@ -513,3 +513,21 @@ fn effective_host(request: &Request, tls: bool) -> Result<HeaderValue, ()> {
|
|||||||
request.headers().get(HOST).cloned().ok_or(())
|
request.headers().get(HOST).cloned().ok_or(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod auth_provider_page_interception_tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn provider_failures_and_redirects_bypass_custom_http_error_pages() {
|
||||||
|
for kind in [
|
||||||
|
crate::auth_provider::GatewayFailure::Connection,
|
||||||
|
crate::auth_provider::GatewayFailure::Timeout,
|
||||||
|
crate::auth_provider::GatewayFailure::Verification,
|
||||||
|
] {
|
||||||
|
let response = crate::auth_provider::gateway_failure("Future", kind);
|
||||||
|
assert!(!should_replace_error_page(&response, false));
|
||||||
|
}
|
||||||
|
let response = crate::auth_provider::deny(StatusCode::UNAUTHORIZED);
|
||||||
|
assert!(!should_replace_error_page(&response, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -144,6 +144,8 @@ fn vhost_cache_revision(vhost: &crate::vhost::Vhost) -> u64 {
|
|||||||
view.content_type_policy = Default::default();
|
view.content_type_policy = Default::default();
|
||||||
view.basic_auth = Default::default();
|
view.basic_auth = Default::default();
|
||||||
view.tinyauth = Default::default();
|
view.tinyauth = Default::default();
|
||||||
|
view.authelia = Default::default();
|
||||||
|
view.authentik = Default::default();
|
||||||
view.acl = None;
|
view.acl = None;
|
||||||
view.acl_profile_id = None;
|
view.acl_profile_id = None;
|
||||||
view.user_agent_blocking = Default::default();
|
view.user_agent_blocking = Default::default();
|
||||||
|
|||||||
+44
-76
@@ -1,12 +1,10 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
auth_provider::{self, GatewayFailure, Identity},
|
||||||
config::{Backend, CompiledBackend, UpstreamProtocol, ValidationPolicy},
|
config::{Backend, CompiledBackend, UpstreamProtocol, ValidationPolicy},
|
||||||
forwarding,
|
forwarding,
|
||||||
};
|
};
|
||||||
use anyhow::{ensure, Context as _, Result};
|
use anyhow::{ensure, Context as _, Result};
|
||||||
use axum::{
|
use axum::{body::Body, response::Response};
|
||||||
body::Body,
|
|
||||||
response::{IntoResponse, Response},
|
|
||||||
};
|
|
||||||
use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, StatusCode, Uri};
|
use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, StatusCode, Uri};
|
||||||
use http_body_util::{BodyExt, Limited};
|
use http_body_util::{BodyExt, Limited};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -61,57 +59,12 @@ pub struct Compiled {
|
|||||||
response_headers: Vec<HeaderName>,
|
response_headers: Vec<HeaderName>,
|
||||||
redirect_forbidden: bool,
|
redirect_forbidden: bool,
|
||||||
}
|
}
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AuthenticationFailure;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum CheckError {
|
enum CheckError {
|
||||||
Connection(anyhow::Error),
|
Connection(anyhow::Error),
|
||||||
InvalidResponse(anyhow::Error),
|
InvalidResponse(anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
enum GatewayFailure {
|
|
||||||
Connection,
|
|
||||||
Timeout,
|
|
||||||
Verification,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gateway_failure(kind: GatewayFailure) -> Response {
|
|
||||||
let (status, html) = match kind {
|
|
||||||
GatewayFailure::Connection => (
|
|
||||||
StatusCode::BAD_GATEWAY,
|
|
||||||
include_str!("../admin/web/error_tinyauth_connection.html"),
|
|
||||||
),
|
|
||||||
GatewayFailure::Timeout => (
|
|
||||||
StatusCode::GATEWAY_TIMEOUT,
|
|
||||||
include_str!("../admin/web/error_tinyauth_timeout.html"),
|
|
||||||
),
|
|
||||||
GatewayFailure::Verification => (
|
|
||||||
StatusCode::BAD_GATEWAY,
|
|
||||||
include_str!("../admin/web/error_tinyauth_verification.html"),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
let mut response = (status, html).into_response();
|
|
||||||
response.headers_mut().insert(
|
|
||||||
"content-type",
|
|
||||||
HeaderValue::from_static("text/html; charset=utf-8"),
|
|
||||||
);
|
|
||||||
response.headers_mut().insert(
|
|
||||||
"cache-control",
|
|
||||||
HeaderValue::from_static("private, no-store"),
|
|
||||||
);
|
|
||||||
response.headers_mut().insert(
|
|
||||||
"x-content-type-options",
|
|
||||||
HeaderValue::from_static("nosniff"),
|
|
||||||
);
|
|
||||||
response.extensions_mut().insert(AuthenticationFailure);
|
|
||||||
response
|
|
||||||
}
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Identity {
|
|
||||||
headers: HeaderMap,
|
|
||||||
}
|
|
||||||
const STANDARD_IDENTITY: [&str; 6] = [
|
const STANDARD_IDENTITY: [&str; 6] = [
|
||||||
"remote-user",
|
"remote-user",
|
||||||
"remote-email",
|
"remote-email",
|
||||||
@@ -252,18 +205,7 @@ impl Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn failure(status: StatusCode) -> Response {
|
fn failure(status: StatusCode) -> Response {
|
||||||
let body = format!(
|
auth_provider::deny(status)
|
||||||
"{} {}\n",
|
|
||||||
status.as_u16(),
|
|
||||||
status.canonical_reason().unwrap_or("Error")
|
|
||||||
);
|
|
||||||
let mut response = (status, body).into_response();
|
|
||||||
response.headers_mut().insert(
|
|
||||||
"cache-control",
|
|
||||||
HeaderValue::from_static("private, no-store"),
|
|
||||||
);
|
|
||||||
response.extensions_mut().insert(AuthenticationFailure);
|
|
||||||
response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_nginx_auth_context(
|
fn apply_nginx_auth_context(
|
||||||
@@ -362,15 +304,24 @@ impl Compiled {
|
|||||||
Ok(Ok(value)) => value,
|
Ok(Ok(value)) => value,
|
||||||
Ok(Err(CheckError::Connection(error))) => {
|
Ok(Err(CheckError::Connection(error))) => {
|
||||||
tracing::warn!(%error, "Tinyauth backend connection failed");
|
tracing::warn!(%error, "Tinyauth backend connection failed");
|
||||||
return Err(gateway_failure(GatewayFailure::Connection));
|
return Err(auth_provider::gateway_failure(
|
||||||
|
"Tinyauth",
|
||||||
|
GatewayFailure::Connection,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Ok(Err(CheckError::InvalidResponse(error))) => {
|
Ok(Err(CheckError::InvalidResponse(error))) => {
|
||||||
tracing::warn!(%error, "Tinyauth returned an invalid verification response");
|
tracing::warn!(%error, "Tinyauth returned an invalid verification response");
|
||||||
return Err(gateway_failure(GatewayFailure::Verification));
|
return Err(auth_provider::gateway_failure(
|
||||||
|
"Tinyauth",
|
||||||
|
GatewayFailure::Verification,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
tracing::warn!("Tinyauth verification timed out");
|
tracing::warn!("Tinyauth verification timed out");
|
||||||
return Err(gateway_failure(GatewayFailure::Timeout));
|
return Err(auth_provider::gateway_failure(
|
||||||
|
"Tinyauth",
|
||||||
|
GatewayFailure::Timeout,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if status.is_success() {
|
if status.is_success() {
|
||||||
@@ -381,7 +332,10 @@ impl Compiled {
|
|||||||
size += value.as_bytes().len();
|
size += value.as_bytes().len();
|
||||||
if size > 8192 {
|
if size > 8192 {
|
||||||
tracing::warn!("Tinyauth identity headers exceed the configured limit");
|
tracing::warn!("Tinyauth identity headers exceed the configured limit");
|
||||||
return Err(gateway_failure(GatewayFailure::Verification));
|
return Err(auth_provider::gateway_failure(
|
||||||
|
"Tinyauth",
|
||||||
|
GatewayFailure::Verification,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
identity.headers.append(name.clone(), value.clone());
|
identity.headers.append(name.clone(), value.clone());
|
||||||
}
|
}
|
||||||
@@ -408,7 +362,10 @@ impl Compiled {
|
|||||||
status = status.as_u16(),
|
status = status.as_u16(),
|
||||||
"Tinyauth returned an unexpected HTTP status"
|
"Tinyauth returned an unexpected HTTP status"
|
||||||
);
|
);
|
||||||
Err(gateway_failure(GatewayFailure::Verification))
|
Err(auth_provider::gateway_failure(
|
||||||
|
"Tinyauth",
|
||||||
|
GatewayFailure::Verification,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
async fn check(
|
async fn check(
|
||||||
&self,
|
&self,
|
||||||
@@ -451,15 +408,26 @@ impl Compiled {
|
|||||||
Ok((parts.status, parts.headers))
|
Ok((parts.status, parts.headers))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Identity {
|
|
||||||
pub fn apply(&self, headers: &mut HeaderMap) {
|
|
||||||
for name in self.headers.keys() {
|
|
||||||
headers.remove(name);
|
|
||||||
}
|
|
||||||
for (name, value) in &self.headers {
|
|
||||||
headers.append(name.clone(), value.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
include!("tests.rs");
|
include!("tests.rs");
|
||||||
|
|
||||||
|
impl crate::auth_provider::Provider for Compiled {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"tinyauth"
|
||||||
|
}
|
||||||
|
fn strip_identity(&self, headers: &mut HeaderMap) {
|
||||||
|
Compiled::strip_identity(self, headers);
|
||||||
|
}
|
||||||
|
fn authorize<'a>(
|
||||||
|
&'a self,
|
||||||
|
incoming: &'a HeaderMap,
|
||||||
|
method: &'a Method,
|
||||||
|
context: &'a forwarding::Context,
|
||||||
|
shutdown: &'a tokio_util::sync::CancellationToken,
|
||||||
|
_cookies: &'a crate::auth_provider::CookieSink,
|
||||||
|
) -> crate::auth_provider::Authorization<'a> {
|
||||||
|
Box::pin(Compiled::authorize(
|
||||||
|
self, incoming, method, context, shutdown,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -208,7 +208,7 @@ mod tests {
|
|||||||
"Tinyauth timeout",
|
"Tinyauth timeout",
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
let response = gateway_failure(kind);
|
let response = auth_provider::gateway_failure("Tinyauth", kind);
|
||||||
assert_eq!(response.status(), status);
|
assert_eq!(response.status(), status);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
response.headers()["content-type"],
|
response.headers()["content-type"],
|
||||||
@@ -218,7 +218,7 @@ mod tests {
|
|||||||
assert_eq!(response.headers()["x-content-type-options"], "nosniff");
|
assert_eq!(response.headers()["x-content-type-options"], "nosniff");
|
||||||
assert!(response
|
assert!(response
|
||||||
.extensions()
|
.extensions()
|
||||||
.get::<AuthenticationFailure>()
|
.get::<auth_provider::AuthenticationResponse>()
|
||||||
.is_some());
|
.is_some());
|
||||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||||
let html = std::str::from_utf8(&body).unwrap();
|
let html = std::str::from_utf8(&body).unwrap();
|
||||||
|
|||||||
+15
-6
@@ -54,7 +54,10 @@ pub struct MethodPolicy {
|
|||||||
}
|
}
|
||||||
impl MethodPolicy {
|
impl MethodPolicy {
|
||||||
pub fn validate(&mut self) -> Result<()> {
|
pub fn validate(&mut self) -> Result<()> {
|
||||||
ensure!(self.methods.len() <= 32, "method policy allows at most 32 methods");
|
ensure!(
|
||||||
|
self.methods.len() <= 32,
|
||||||
|
"method policy allows at most 32 methods"
|
||||||
|
);
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
for method in &mut self.methods {
|
for method in &mut self.methods {
|
||||||
*method = method.trim().to_ascii_uppercase();
|
*method = method.trim().to_ascii_uppercase();
|
||||||
@@ -62,7 +65,10 @@ impl MethodPolicy {
|
|||||||
!method.is_empty() && method.len() <= 32 && method.parse::<http::Method>().is_ok(),
|
!method.is_empty() && method.len() <= 32 && method.parse::<http::Method>().is_ok(),
|
||||||
"invalid HTTP method in method policy"
|
"invalid HTTP method in method policy"
|
||||||
);
|
);
|
||||||
ensure!(seen.insert(method.clone()), "duplicate HTTP method in method policy");
|
ensure!(
|
||||||
|
seen.insert(method.clone()),
|
||||||
|
"duplicate HTTP method in method policy"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
ensure!(
|
ensure!(
|
||||||
self.mode == MethodPolicyMode::Off || !self.methods.is_empty(),
|
self.mode == MethodPolicyMode::Off || !self.methods.is_empty(),
|
||||||
@@ -115,9 +121,7 @@ impl ContentTypePolicy {
|
|||||||
anyhow::bail!("invalid blocked Content-Type: expected type/subtype");
|
anyhow::bail!("invalid blocked Content-Type: expected type/subtype");
|
||||||
};
|
};
|
||||||
ensure!(
|
ensure!(
|
||||||
mime_token(kind)
|
mime_token(kind) && (subtype == "*" || mime_token(subtype)) && value.len() <= 255,
|
||||||
&& (subtype == "*" || mime_token(subtype))
|
|
||||||
&& value.len() <= 255,
|
|
||||||
"invalid blocked Content-Type: use type/subtype or type/* without parameters"
|
"invalid blocked Content-Type: use type/subtype or type/* without parameters"
|
||||||
);
|
);
|
||||||
ensure!(
|
ensure!(
|
||||||
@@ -346,6 +350,10 @@ pub struct Vhost {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tinyauth: crate::tinyauth::Settings,
|
pub tinyauth: crate::tinyauth::Settings,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub authelia: crate::authelia::Settings,
|
||||||
|
#[serde(default)]
|
||||||
|
pub authentik: crate::authentik::Settings,
|
||||||
|
#[serde(default)]
|
||||||
pub certificate_id: Option<String>,
|
pub certificate_id: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub allow_certificate_mismatch: bool,
|
pub allow_certificate_mismatch: bool,
|
||||||
@@ -379,7 +387,8 @@ pub struct Vhost {
|
|||||||
pub headers: HeaderRules,
|
pub headers: HeaderRules,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub security_headers: SecurityHeaders,
|
pub security_headers: SecurityHeaders,
|
||||||
#[serde(default)] pub user_agent_blocking: crate::user_agent::Policy,
|
#[serde(default)]
|
||||||
|
pub user_agent_blocking: crate::user_agent::Policy,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub proxy_timeouts: ProxyTimeouts,
|
pub proxy_timeouts: ProxyTimeouts,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
|
|||||||
@@ -242,12 +242,14 @@ impl Vhost {
|
|||||||
if let Some(server) = &self.server_header {
|
if let Some(server) = &self.server_header {
|
||||||
server.validate()?;
|
server.validate()?;
|
||||||
}
|
}
|
||||||
self.tinyauth.validate()?;
|
|
||||||
self.basic_auth.compile()?;
|
self.basic_auth.compile()?;
|
||||||
ensure!(
|
crate::auth_provider::Selection {
|
||||||
!(self.tinyauth.enabled && self.basic_auth.enabled),
|
basic_enabled: self.basic_auth.enabled,
|
||||||
"choose either Tinyauth or Basic authentication"
|
tinyauth: &self.tinyauth,
|
||||||
);
|
authelia: &self.authelia,
|
||||||
|
authentik: &self.authentik,
|
||||||
|
}
|
||||||
|
.validate()?;
|
||||||
self.pages.validate()?;
|
self.pages.validate()?;
|
||||||
self.transform
|
self.transform
|
||||||
.resolve(&crate::transform::Settings::default())?;
|
.resolve(&crate::transform::Settings::default())?;
|
||||||
|
|||||||
Reference in new issue
Block a user