mirror of
https://github.com/whit3rabbit/anyllm-proxy.git
synced 2026-09-21 08:00:48 +00:00
Merge PR #41: fix: reject unauthenticated loopback requests
This commit is contained in:
+4
-6
@@ -24,12 +24,10 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follo
|
||||
## [0.16.0] - 2026-07-16
|
||||
|
||||
### Changed
|
||||
- Auth default is now **loopback-open** instead of reject-all. With no `PROXY_API_KEYS`,
|
||||
no `PROXY_OPEN_RELAY`, no virtual keys and no OIDC, the proxy accepts unauthenticated
|
||||
requests from localhost only; LAN/remote peers still get `401`. Decision uses the real
|
||||
TCP peer (`ConnectInfo`), not the spoofable `X-Forwarded-For`. Set `PROXY_API_KEYS` when
|
||||
running behind a reverse proxy. `GET /admin/api/status` now reports `auth_mode`
|
||||
(`keys` / `open_relay` / `loopback_only`) and `proxy_key_count`.
|
||||
- Auth default is **reject-all**. With no `PROXY_API_KEYS`, no `PROXY_OPEN_RELAY`, no
|
||||
virtual keys, and no OIDC, every proxy request returns `401`, including from localhost.
|
||||
Set `PROXY_API_KEYS` to allow authenticated access. `GET /admin/api/status` reports `auth_mode`
|
||||
(`keys` / `open_relay` / `auth_required`) and `proxy_key_count`.
|
||||
- Proxy start-up now pre-checks the listen port and fails fast with a hint when it is
|
||||
already in use; the `wait_for_port` readiness timeout rose from 10s to 30s.
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ Five-crate Cargo workspace: `providers` (metadata catalog), `client` (Anthropic
|
||||
- **CSRF tokens are one-time-use.** Fetch a fresh token from `GET /admin/csrf-token` before each admin POST/PUT/DELETE. The SPA does this automatically; scripts must too.
|
||||
- **Admin UI defaults on for bare invocation.** Running `anyllm-proxy` with **no args** starts proxy + admin UI and auto-opens the default browser to the admin page (the zero-arg desktop default). Passing `--webui`/`--admin` (or `WEBUI=1`/`ADMIN=1` env) forces it on alongside other args but does **not** open a browser. Passing any other arg (e.g. `--env-file`, `--redact-secrets`) keeps the proxy CLI-only. `DISABLE_ADMIN=1` force-disables it in all cases. Gate + zero-arg logic live in `main_helpers/bootstrap.rs` (`admin_enabled`/`admin_requested`/`is_default_launch`); browser open in `main_helpers/browser.rs`.
|
||||
- **Virtual key OnceLock in tests.** `set_virtual_keys` uses a global `OnceLock<DashMap>`. Integration tests in `crates/proxy/tests/virtual_keys.rs` use a shared `OnceLock` to avoid conflicts.
|
||||
- **Auth defaults to loopback-open.** With no `PROXY_API_KEYS`, no `PROXY_OPEN_RELAY`, no virtual keys and no OIDC, requests from a loopback TCP peer are accepted; LAN/remote peers get 401. Gate: `no_auth_configured() && peer_is_loopback()` at the top of `validate_auth` (`server/middleware/auth.rs`), using `ConnectInfo` (the proxy is now served with `into_make_service_with_connect_info`), NOT `X-Forwarded-For`. `effective_auth_mode()` (`keys`/`open_relay`/`loopback_only`) is surfaced via `GET /admin/api/status` and the admin UI banner.
|
||||
- **Auth defaults to reject-all.** Without `PROXY_API_KEYS`, `PROXY_OPEN_RELAY=true`, virtual keys, or OIDC, every request gets 401, including from localhost. `effective_auth_mode()` (`keys`/`open_relay`/`auth_required`) is surfaced via `GET /admin/api/status` and the admin UI banner.
|
||||
- **Admin rate limiter resets on restart.** 10 RPM per source IP, in-memory sliding window. `set_admin_rpm()` overrides for tests.
|
||||
- **Docker admin needs `ADMIN_BIND=0.0.0.0`.** Default binds to 127.0.0.1 which is unreachable from outside the container.
|
||||
- **PLAN.md references in source comments are stale.** Some files reference line ranges in a removed PLAN.md.
|
||||
|
||||
@@ -33,7 +33,7 @@ cargo test --test live_api -- --ignored --test-threads=1 # needs real key
|
||||
- **Admin UI defaults on for bare invocation.** Bare `anyllm-proxy` (no args) starts proxy + admin UI and auto-opens the browser (zero-arg default). `--webui`/`--admin` or `WEBUI=1`/`ADMIN=1` force it on with other args (no browser). Any other arg keeps it CLI-only. `DISABLE_ADMIN=1` force-disables. Single gate: `main_helpers::bootstrap::admin_enabled` (used by `main.rs` and `init_admin`); browser open only on `is_default_launch` via `main_helpers::browser::open`.
|
||||
- **Runtime smoke-test in isolation.** Use a fresh `ANYLLM_HOME=$(mktemp -d)` + non-default `LISTEN_PORT`/`ADMIN_PORT`: the real `~/.anyllm` DB's persisted admin config overrides can hang `--webui` *before* the servers bind (stalls right after "applied config overrides from database"), and port 3000 is often held by other dev servers (giving false `200`s from something that isn't the proxy). `--redact-secrets`/`REDACT_SECRETS` also adds multi-second startup — allow more time or omit for quick checks.
|
||||
- **`main_helpers` (bin-only) tests can't use `ENV_TEST_LOCK`.** It's `pub(crate)` in the lib crate, unreachable from the bin crate. For bin-only code that reads env, split the pure logic (e.g. `admin_requested(args)`) from the env read (`admin_enabled`) and unit-test the pure part with no env mutation, instead of trying to serialize on the lock.
|
||||
- **Auth defaults to loopback-open.** No `PROXY_API_KEYS`, no `PROXY_OPEN_RELAY`, no virtual keys, no OIDC => loopback peers accepted, LAN/remote get 401. Gate is `no_auth_configured() && peer_is_loopback()` at the top of `validate_auth` (`middleware/auth.rs`); proxy is served with `into_make_service_with_connect_info` so `ConnectInfo` is present. `effective_auth_mode()` feeds `GET /admin/api/status` (`auth_mode`) and the admin UI banner.
|
||||
- **Auth defaults to reject-all.** Without `PROXY_API_KEYS`, `PROXY_OPEN_RELAY=true`, virtual keys, or OIDC, every request gets 401, including from localhost. `effective_auth_mode()` feeds `GET /admin/api/status` (`auth_mode`) and the admin UI banner.
|
||||
- **CSRF tokens are one-time-use.** Fetch a fresh one from `GET /admin/csrf-token` before each admin POST/PUT/DELETE. Scripts must too.
|
||||
- **Live admin-endpoint smoke:** run with `ADMIN_TOKEN=<32+ chars> ... --webui` (admin on :3001). GET needs `Authorization: Bearer $ADMIN_TOKEN`. POST/PUT/DELETE ALSO need CSRF: `GET /admin/csrf-token` with a cookie jar (`curl -c jar`), then resend with `-b jar` + `X-CSRF-Token: <token>` (header must equal the cookie). Missing/mismatched CSRF returns 403 before your handler runs.
|
||||
- **`main_helpers` is bin-only** (declared in `main.rs`, NOT `lib.rs`). Library code (anything reached via `crate::` at runtime, e.g. `optimizer.rs`) cannot use `crate::main_helpers::bootstrap::*` — it won't compile. The data-dir/home helpers live in `crate::config::helpers::{resolve_data_dir, home_dir}`; use those from lib code.
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -8,10 +8,10 @@ export interface ProxyStatus {
|
||||
proxy_running: boolean
|
||||
/**
|
||||
* Effective proxy auth posture. "keys": a key is required. "open_relay": any
|
||||
* key accepted on all interfaces. "loopback_only": no auth, localhost open and
|
||||
* LAN rejected (the default). Drives the top-of-app warning banner.
|
||||
* key accepted on all interfaces. "auth_required": no authentication source is
|
||||
* configured, so all requests are rejected. Drives the top-of-app warning banner.
|
||||
*/
|
||||
auth_mode: 'keys' | 'open_relay' | 'loopback_only'
|
||||
auth_mode: 'keys' | 'open_relay' | 'auth_required'
|
||||
/** Number of distinct static PROXY_API_KEYS entries. */
|
||||
proxy_key_count: number
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const box = (accent: string): CSSProperties => ({
|
||||
/**
|
||||
* Site-wide warning banners rendered above every tab. Reflects live state
|
||||
* (cleared automatically when fixed): proxy auth is open (open_relay) or unset
|
||||
* (loopback_only). useStatus is React Query-cached, so no extra fetch.
|
||||
* (auth_required). useStatus is React Query-cached, so no extra fetch.
|
||||
*
|
||||
* Deliberately does NOT warn on an empty Models tab: that lists only
|
||||
* model-router deployments (virtual model aliases), which are optional. A
|
||||
@@ -35,13 +35,11 @@ export default function AppBanner() {
|
||||
<span className="mono">PROXY_API_KEYS</span> to require a key.
|
||||
</div>,
|
||||
)
|
||||
} else if (status?.auth_mode === 'loopback_only') {
|
||||
} else if (status?.auth_mode === 'auth_required') {
|
||||
banners.push(
|
||||
<div key="auth" style={box('var(--warn)')}>
|
||||
<strong>No API key set.</strong> The proxy is open on localhost only;
|
||||
LAN/remote requests are rejected. Set{' '}
|
||||
<span className="mono">PROXY_API_KEYS</span> to require a key for remote
|
||||
access.
|
||||
<strong>No API key set.</strong> The proxy rejects all requests. Set{' '}
|
||||
<span className="mono">PROXY_API_KEYS</span> to allow authenticated access.
|
||||
</div>,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ pub struct ProxyStatus {
|
||||
/// Whether the proxy's own port accepts a TCP connection right now.
|
||||
pub proxy_running: bool,
|
||||
/// Effective proxy auth posture: `"keys"` (enforced), `"open_relay"` (any
|
||||
/// key accepted on all interfaces), or `"loopback_only"` (no auth; localhost
|
||||
/// open, LAN rejected, the default). Drives the admin UI warning banner.
|
||||
/// key accepted on all interfaces), or `"auth_required"` (no authentication
|
||||
/// source configured; all requests rejected). Drives the admin UI warning banner.
|
||||
pub auth_mode: crate::server::middleware::EffectiveAuthMode,
|
||||
/// Number of distinct static `PROXY_API_KEYS` entries (deduplicated).
|
||||
pub proxy_key_count: usize,
|
||||
|
||||
@@ -336,8 +336,7 @@ pub async fn async_main(args: Vec<String>, data_dir: PathBuf) {
|
||||
let (shutdown_tx, mut shutdown_rx1) = tokio::sync::watch::channel(false);
|
||||
|
||||
let proxy_handle = tokio::spawn(async move {
|
||||
// connect-info supplies the TCP peer SocketAddr to request extensions;
|
||||
// auth's loopback-open default and the IP allowlist read it.
|
||||
// ConnectInfo supplies the TCP peer SocketAddr for the IP allowlist.
|
||||
axum::serve(
|
||||
proxy_listener,
|
||||
app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
|
||||
|
||||
@@ -74,7 +74,7 @@ pub struct VirtualKeyContext {
|
||||
pub(crate) period_reset: Option<String>,
|
||||
}
|
||||
|
||||
/// Which of `validate_auth`'s four success paths authenticated this request.
|
||||
/// Which of `validate_auth`'s success paths authenticated this request.
|
||||
/// Inserted into request extensions at every success branch so a handler can
|
||||
/// tell what kind of credential got it in -- used by `ANTHROPIC_FORWARD_CLIENT_AUTH`
|
||||
/// to decide whether it's safe to forward that same credential upstream as the
|
||||
@@ -93,10 +93,6 @@ pub enum ClientAuthPath {
|
||||
VirtualKey,
|
||||
/// `PROXY_OPEN_RELAY=true`: any non-empty credential accepted.
|
||||
OpenRelay,
|
||||
/// Loopback-open default: no proxy auth configured at all, request came
|
||||
/// from a loopback peer. Not a real credential, so never forwarded upstream
|
||||
/// (`client_auth_forwardable` returns false for it).
|
||||
LoopbackOpen,
|
||||
}
|
||||
|
||||
/// Controls which authentication paths are active.
|
||||
@@ -155,7 +151,7 @@ static ALLOWED_KEY_HASHES: LazyLock<Vec<[u8; 32]>> = LazyLock::new(|| {
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
// Posture logging (open-relay warn / loopback-open warn) is emitted once at
|
||||
// Posture logging is emitted once at
|
||||
// startup by `log_effective_auth_posture`, AFTER virtual keys and OIDC are
|
||||
// registered, so it reflects the true posture instead of the partial
|
||||
// static-key/open-relay state visible at this LazyLock's init time.
|
||||
@@ -202,10 +198,7 @@ fn has_virtual_keys() -> bool {
|
||||
VIRTUAL_KEYS.get().map(|m| !m.is_empty()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Whether the proxy has NO auth configured at all: no static keys, no open
|
||||
/// relay, no virtual keys, no OIDC. In this state the proxy falls back to the
|
||||
/// loopback-open default (see [`validate_auth`]): localhost is accepted without
|
||||
/// a credential, LAN/remote peers are still rejected with 401.
|
||||
/// Whether the proxy has no usable authentication source configured.
|
||||
fn no_auth_configured() -> bool {
|
||||
ALLOWED_KEY_HASHES.is_empty()
|
||||
&& !*OPEN_RELAY
|
||||
@@ -225,21 +218,21 @@ pub enum EffectiveAuthMode {
|
||||
OpenRelay,
|
||||
/// At least one static key, virtual key, or OIDC configured (auth enforced).
|
||||
Keys,
|
||||
/// Nothing configured: localhost open, LAN rejected (the default).
|
||||
LoopbackOnly,
|
||||
/// Nothing configured: every request is rejected.
|
||||
AuthRequired,
|
||||
}
|
||||
|
||||
/// The effective auth posture for this process:
|
||||
/// - [`EffectiveAuthMode::OpenRelay`] when `PROXY_OPEN_RELAY=true` (any non-empty
|
||||
/// key accepted on all interfaces).
|
||||
/// - [`EffectiveAuthMode::LoopbackOnly`] when nothing is configured (localhost
|
||||
/// open, LAN rejected, the default).
|
||||
/// - [`EffectiveAuthMode::AuthRequired`] when nothing is configured (all
|
||||
/// requests are rejected, the default).
|
||||
/// - [`EffectiveAuthMode::Keys`] otherwise (auth enforced).
|
||||
pub fn effective_auth_mode() -> EffectiveAuthMode {
|
||||
if open_relay_active() {
|
||||
EffectiveAuthMode::OpenRelay
|
||||
} else if no_auth_configured() {
|
||||
EffectiveAuthMode::LoopbackOnly
|
||||
EffectiveAuthMode::AuthRequired
|
||||
} else {
|
||||
EffectiveAuthMode::Keys
|
||||
}
|
||||
@@ -249,18 +242,18 @@ pub fn effective_auth_mode() -> EffectiveAuthMode {
|
||||
/// from `async_main` after virtual keys and OIDC are registered, so the message
|
||||
/// is accurate even for an OIDC-only or virtual-key-only deployment (unlike the
|
||||
/// old boot-time warn, which fired from the static-key `LazyLock` before those
|
||||
/// sources existed and could mislabel such setups as "loopback-only").
|
||||
/// sources existed and could mislabel such setups as unconfigured).
|
||||
pub fn log_effective_auth_posture() {
|
||||
match effective_auth_mode() {
|
||||
EffectiveAuthMode::OpenRelay => tracing::warn!(
|
||||
"PROXY_OPEN_RELAY=true: proxy accepts ANY non-empty key on all \
|
||||
interfaces. Set PROXY_API_KEYS to restrict access."
|
||||
),
|
||||
EffectiveAuthMode::LoopbackOnly => tracing::warn!(
|
||||
EffectiveAuthMode::AuthRequired => tracing::warn!(
|
||||
"No PROXY_API_KEYS, virtual keys, OIDC, or PROXY_OPEN_RELAY set: \
|
||||
accepting unauthenticated requests from localhost only; LAN/remote \
|
||||
peers get 401. Set PROXY_API_KEYS to require a key, or \
|
||||
PROXY_OPEN_RELAY=true to accept any key on all interfaces."
|
||||
rejecting all proxy requests. Set PROXY_API_KEYS to allow \
|
||||
authenticated access, or PROXY_OPEN_RELAY=true to accept any key \
|
||||
on all interfaces."
|
||||
),
|
||||
EffectiveAuthMode::Keys => {
|
||||
tracing::info!("proxy auth enforced via keys / virtual keys / OIDC")
|
||||
@@ -268,35 +261,6 @@ pub fn log_effective_auth_posture() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the request's TCP peer is a loopback address. Reads `ConnectInfo`
|
||||
/// (the real connection peer), NOT `X-Forwarded-For`, which is client-spoofable.
|
||||
/// Fails closed: if `ConnectInfo` is absent (proxy not served with connect
|
||||
/// info), returns false so the loopback-open default never accidentally opens.
|
||||
fn peer_is_loopback(request: &Request<Body>) -> bool {
|
||||
request
|
||||
.extensions()
|
||||
.get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
|
||||
.map(|ci| is_loopback_ip(ci.0.ip()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Loopback test that also accepts IPv4-mapped IPv6 (`::ffff:127.0.0.1`):
|
||||
/// dual-stack listeners present IPv4 loopback peers as mapped v6, which std's
|
||||
/// `Ipv6Addr::is_loopback()` (only `::1`) would wrongly classify as remote and
|
||||
/// reject with 401. Never widens the surface beyond a genuine loopback peer.
|
||||
fn is_loopback_ip(ip: std::net::IpAddr) -> bool {
|
||||
match ip {
|
||||
std::net::IpAddr::V4(v4) => v4.is_loopback(),
|
||||
std::net::IpAddr::V6(v6) => {
|
||||
v6.is_loopback()
|
||||
|| v6
|
||||
.to_ipv4_mapped()
|
||||
.map(|v4| v4.is_loopback())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// True when forwarding the client's own credential upstream
|
||||
/// (`ANTHROPIC_FORWARD_CLIENT_AUTH`) could let different callers each
|
||||
/// redirect the real Anthropic credential: 2+ distinct static keys with no
|
||||
@@ -310,7 +274,8 @@ pub fn forward_client_auth_misconfigured(key_count: usize, open_relay: bool) ->
|
||||
|
||||
/// Validate that the request carries a valid API key.
|
||||
/// If `PROXY_API_KEYS` is set, the caller's key must be in the allowlist.
|
||||
/// Otherwise, any non-empty key is accepted (backward-compatible open mode).
|
||||
/// `PROXY_OPEN_RELAY=true` explicitly accepts any non-empty key; otherwise,
|
||||
/// requests are rejected when no authentication source is configured.
|
||||
///
|
||||
/// Anthropic: <https://docs.anthropic.com/en/api/messages>
|
||||
#[allow(clippy::result_large_err)]
|
||||
@@ -319,20 +284,6 @@ pub async fn validate_auth(
|
||||
mut request: Request<Body>,
|
||||
next: Next,
|
||||
) -> Result<Response, Response> {
|
||||
// Loopback-open default: when NO proxy auth is configured (no static keys,
|
||||
// no open relay, no virtual keys, no OIDC), accept requests whose TCP peer
|
||||
// is loopback so `localhost` works out of the box. LAN/remote peers fall
|
||||
// through to the checks below and get 401. Runs before credential parsing so
|
||||
// a header-less localhost call succeeds.
|
||||
// ponytail: trusts the TCP peer. Behind a reverse proxy on localhost every
|
||||
// request looks loopback -> effectively open; set PROXY_API_KEYS then.
|
||||
if no_auth_configured() && peer_is_loopback(&request) {
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(ClientAuthPath::LoopbackOpen);
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
|
||||
// Accept x-api-key (Anthropic), x-goog-api-key (Gemini CLI), or Authorization: Bearer.
|
||||
let api_key = headers
|
||||
.get("x-api-key")
|
||||
@@ -598,12 +549,11 @@ pub async fn validate_auth(
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
|
||||
// No match found: reject. `no_auth_configured()` here means the peer is
|
||||
// non-loopback (loopback short-circuits at the top), so explain the
|
||||
// localhost-only default rather than a generic "not configured".
|
||||
// No match found: reject. When no auth source is configured, fail closed
|
||||
// rather than exposing a browser-reachable local proxy.
|
||||
let message = if no_auth_configured() {
|
||||
"This proxy accepts unauthenticated requests from localhost only. \
|
||||
Set PROXY_API_KEYS to allow authenticated remote access."
|
||||
"Authentication is not configured. Set PROXY_API_KEYS to allow \
|
||||
authenticated access."
|
||||
} else {
|
||||
"Invalid API key."
|
||||
};
|
||||
|
||||
@@ -71,30 +71,3 @@ fn forward_client_auth_allows_exactly_one_key() {
|
||||
fn forward_client_auth_allows_zero_keys() {
|
||||
assert!(!forward_client_auth_misconfigured(0, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_is_loopback_reads_connect_info() {
|
||||
use axum::extract::ConnectInfo;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
let loopback = |addr: &str| {
|
||||
let mut req = axum::http::Request::new(axum::body::Body::empty());
|
||||
req.extensions_mut()
|
||||
.insert(ConnectInfo(addr.parse::<SocketAddr>().unwrap()));
|
||||
peer_is_loopback(&req)
|
||||
};
|
||||
assert!(loopback("127.0.0.1:5000"));
|
||||
assert!(loopback("[::1]:5000"));
|
||||
// IPv4-mapped IPv6: dual-stack listeners present IPv4 loopback peers as
|
||||
// `::ffff:127.0.0.1`, which std `Ipv6Addr::is_loopback()` (only `::1`)
|
||||
// would reject. Must still count as loopback.
|
||||
assert!(loopback("[::ffff:127.0.0.1]:5000"));
|
||||
// ... but a mapped non-loopback IPv4 must not.
|
||||
assert!(!loopback("[::ffff:192.168.1.5]:5000"));
|
||||
assert!(!loopback("192.168.1.5:5000"));
|
||||
assert!(!loopback("10.0.0.3:5000"));
|
||||
|
||||
// No ConnectInfo present -> fail closed (never auto-open).
|
||||
let bare = axum::http::Request::new(axum::body::Body::empty());
|
||||
assert!(!peer_is_loopback(&bare));
|
||||
}
|
||||
|
||||
@@ -161,9 +161,6 @@ mod tests {
|
||||
assert!(client_auth_forwardable(Some(ClientAuthPath::OpenRelay)));
|
||||
assert!(!client_auth_forwardable(Some(ClientAuthPath::VirtualKey)));
|
||||
assert!(!client_auth_forwardable(Some(ClientAuthPath::OidcJwt)));
|
||||
// Loopback-open is not a real credential, so it must never be forwarded
|
||||
// upstream (locks the security-relevant default against future regressions).
|
||||
assert!(!client_auth_forwardable(Some(ClientAuthPath::LoopbackOpen)));
|
||||
assert!(!client_auth_forwardable(None));
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -55,11 +55,11 @@ Every proxy API endpoint except `/health` requires authentication.
|
||||
|
||||
Unauthenticated requests return `401 Unauthorized` with an Anthropic-shaped error body.
|
||||
|
||||
### Loopback-open default
|
||||
### Authentication default
|
||||
|
||||
When **no** proxy auth is configured (no `PROXY_API_KEYS`, no `PROXY_OPEN_RELAY=true`, no virtual keys, no OIDC), the proxy accepts unauthenticated requests **from loopback (localhost) peers only**; LAN/remote peers still get `401`. This makes local dev work out of the box while keeping the port closed to the network. The decision uses the real TCP peer address (`ConnectInfo`), not the client-spoofable `X-Forwarded-For`.
|
||||
When **no** proxy auth is configured (no `PROXY_API_KEYS`, no `PROXY_OPEN_RELAY=true`, no virtual keys, no OIDC), the proxy rejects every request with `401`, including requests from localhost. Set `PROXY_API_KEYS` to allow authenticated access, or set `PROXY_OPEN_RELAY=true` only for explicitly open local development.
|
||||
|
||||
Caveat: behind a reverse proxy running on localhost, every request appears to come from loopback, so the proxy is effectively open. Set `PROXY_API_KEYS` in that topology. The effective posture is reported as `auth_mode` (`keys` / `open_relay` / `loopback_only`) by `GET /admin/api/status` and surfaced as a warning banner in the admin UI.
|
||||
The effective posture is reported as `auth_mode` (`keys` / `open_relay` / `auth_required`) by `GET /admin/api/status` and surfaced as a warning banner in the admin UI.
|
||||
|
||||
### IP allowlist
|
||||
|
||||
|
||||
Reference in New Issue
Block a user