FAS (Forward Auth Service) is a lightweight visitor access control service designed specifically for Traefik's ForwardAuth middleware. Written in Rust using the axum web framework, it provides visitor registration, granular ACL control (using regular expressions), cookie domain scope control, rate-limiting, and an administrative dashboard to manage visitors and configuration.
- Forward Auth Middleware Compatible: Inspects and intercepts traffic via the standard
/_authroute. - Granular ACL Control: Define access control rules by HTTP Method, Domain, and URL Path using regular expressions (Regex).
- Deny-First Priority: Deny rules take precedence over allow rules.
- Default Fallback Rules: If no rules are defined in
acl.yaml,✅ allow_all(allow everything) and🚫 deny_all(deny everything) are automatically initialized. New visitors receive an empty rule (""), defaulting to deny.
- Cookie Domain Scope Control:
- Default: Omit the
Domainattribute inSet-Cookieheaders, restricting cookies strictly to exact host matches (excluding subdomains). - Configurable Scope Mapping: Map domain regex patterns to parent domain levels (e.g.,
^.*\.b\.a\.com$: 1writesDomain=a.com) or explicit domain strings.
- Default: Omit the
- YAML Configuration & Live Admin Editor: Stored in standard YAML (
acl.yaml). The Admin Panel includes a tab with a live editor featuring YAML syntax & regex linting and validation. - User Expiration (
expire_at): Tracks visitor expiration timestampexpire_at. If missing, it is automatically computed based on default TTLs. Expired users are automatically purged by the background maintenance job. - In-Memory Store with Debounced Persistence: Fast lookups with asynchronous JSONL persistence, debouncing disk writes (at most once every 30s) to maximize throughput and minimize disk wear.
- IP-Based Rate Limiting: Limit unapproved requests to 1 request per 5 seconds per IP, returning a
429 Too Many Requestspage with an interactive countdown timer. - Multi-lingual Support: Automatically detects language preferences (
Accept-Languageheaders) and serves pages in English (en) or Chinese (zh-CN). - Secure Dashboard: Tabbed administrative interface to manage visitor ACL rules, search/filter users, and edit ACL/Cookie YAML configuration.
- Shared Secret Protection: Optionally enforce a
X-Shared-Secretheader (set viaFAS_SHARED_SECRET) on all endpoints except/_auth, ensuring the admin portal and APIs are only reachable through the upstream proxy (e.g. Traefik) and not directly from other pods in the cluster. - Secure Runtime Container: Multi-arch Docker images built on secure, ultra-minimal
gcr.io/distroless/cc-debian12.
The application is structured cleanly:
src/main.rs: Configures logging, spawns background tasks (saving, purging, rate-limit cleanup), and sets up Axum router.src/config.rs: Defines environment configuration settings.src/acl.rs: Core ACL engine, regex pattern compilation, YAML validation, and cookie domain scope resolution.src/store.rs: Manages in-memory user data,expire_atcalculation, JSONL persistence, and TTL purging.src/handlers.rs: Axum handlers for/_auth, config validation/saving, stats, user management, and admin UI.src/templates.rs: UI templates for visitor cards, rate-limiting pages, and the tabbed admin dashboard.src/i18n.rs: Handles localization dictionaries (English & Chinese).
| Environment Variable | Description | Default Value |
|---|---|---|
FAS_PORT |
Port the web server binds to | 8080 |
FAS_DATA_FILE |
Path where user data is stored in JSONL format | /data/fas.jsonl |
FAS_ACL_FILE |
Path where ACL & Cookie YAML configuration is stored | /data/acl.yaml |
FAS_COOKIE_MAX_AGE |
Duration of the session cookie fas_sid in seconds (also defines record retention for allowed users) |
7776000 (90 days) |
FAS_COOKIE_SECURE |
Set Secure flag on session cookies (true/false). Set to false for local plain HTTP testing. |
true |
FAS_TEAPOD_PATH |
Secret subpath for credential verification endpoint | /teapod |
FAS_UNAPPROVED_TTL_SECS |
Soft expiration for unapproved/denied visitor IDs in seconds | 3600 (1 hour) |
FAS_PURGE_INTERVAL_SECS |
Interval at which database TTL purges run | |
FAS_RATE_LIMIT_WINDOW_SECS |
Minimum interval between requests for unapproved visitors | 5 (5 seconds) |
FAS_SAVE_INTERVAL_SECS |
Throttle time before saving dirty state to disk | 30 (30 seconds) |
FAS_SHARED_SECRET |
If set, all requests (except /_auth and health probes) must include X-Shared-Secret: <value> — use this to prevent direct pod-to-pod access in Kubernetes; set the header in proxy and FAS will reject requests without it |
(unset — no check) |
FAS_LOG_LEVEL / LOG_LEVEL |
Logging level (debug, info, warn, error, trace) |
info |
FAS_LOG_FORMAT / LOG_FORMAT / FAS_LOG_JSON |
Log format (json or text) — structured JSON logging to stdout |
json |
FAS provides probe endpoints for Kubernetes and K9s health monitoring:
- Liveness Probes:
/livez,/_livez,/live,/healthz,/_health,/health - Readiness Probes:
/readyz,/_readyz,/ready
These endpoints return 200 OK with body "OK" and automatically bypass FAS_SHARED_SECRET enforcement.
For Traefik and Nginx, ForwardAuth points to http://fas:8080/_auth. The original path is extracted from X-Forwarded-Uri or X-Original-URI.
For Envoy HTTP ext_authz, Envoy appends the target path to the auth path (e.g., visiting /abc results in Envoy calling /_auth/abc). FAS extracts the target URL path from the suffix after /_auth (/abc) and evaluates it against ACL rules.
The acl.yaml file defines cookie scope rules, authentication policies, and user ACL rules:
# Cookie Scope Mappings
# Keys are base domains. Subdomains at all levels match automatically (e.g. a.com matches b.a.com and c.b.a.com).
# More specific keys (e.g. x.a.com) override broader parent domain rules (e.g. a.com).
# Values can be a direct integer level or a mapping specifying 'level' and 'path'.
#
# Default behavior (if the request host does NOT match any configured domain):
# - FAS performs authentication check only and will NEVER set or refresh cookies for the unconfigured domain.
# - Accessing the Teapod credential verification page on an unconfigured domain returns HTTP 404 Not Found.
#
# All cookies generated by FAS automatically enforce 'Secure' (configurable via FAS_COOKIE_SECURE), 'HttpOnly', and 'SameSite=Lax'.
cookie:
a.com:
level: 0 # Matches *.a.com -> Domain=a.com
path: "/" # Cookie path attribute
x.a.com:
level: 0 # Overrides a.com for *.x.a.com -> Domain=x.a.com
path: "/subapp"
internal.net: 1 # Matches *.internal.net -> Domain=internal.net
# Authentication Sources & Proxy Error Configuration
# Explicitly configure allowed authentication sources (cookie, regular auth header, proxy auth header).
# proxy_error_code configures the HTTP status code returned when proxy authentication fails (default 409, or e.g. 405 to hide proxy).
auth_config:
enable_cookie: true # Enable browser session cookie (fas_sid)
enable_header: true # Enable Authorization Bearer/Basic & X-Auth-Token headers
enable_proxy_header: true # Enable Proxy-Authorization header
proxy_error_code: 409 # Status code returned on unauthorized proxy request
# ACL Rule Definitions (Regex Patterns)
acl_rules:
"✅ allow_all":
allow:
- method: ".*"
domain: ".*"
path: ".*"
"🚫 deny_all":
deny:
- method: ".*"
domain: ".*"
path: ".*"
developer_access:
auth_config: # Rule-level auth override
enable_cookie: true
enable_header: true
enable_proxy_header: true
proxy_error_code: 405 # Return 405 Method Not Allowed on proxy auth failure
allow:
- method: "^(GET|HEAD)$"
domain: "^.*\\.dev\\.example\\.com$"
path: "^/api/.*$"
- method: "^POST$"
domain: "^dev\\.example\\.com$"
path: "^/api/v1/.*$"
deny:
- method: ".*"
domain: ".*"
path: "^/admin/.*$"
- method: "^DELETE$"
domain: ".*"
path: ".*"FAS provides a credential verification endpoint (defaulting to /_auth/teapod or configured via FAS_TEAPOD_PATH to any custom secret path):
- HTTP Status Code:
418 I'm a teapotreturned upon successful authentication. - Interactive UI:
- Displays user's credentials and SID token.
- Token is masked as
*by default with a toggle show/hide button (👁️ Show Token/🙈 Hide Token). - Includes a one-click
📋 Copy Tokenbutton with instant feedback. - Lists header usage information according to enabled authentication modes (
Cookie: fas_sid=<token>,Authorization: Bearer <token>,Proxy-Authorization: Bearer <token>).
- If authentication fails or visitor is pending approval, standard unauthorized status (401, 409, or 405) is returned.
- Rust & Cargo (1.71.0 or higher)
# Start the server locally
cargo run# Run unit and integration test suite
cargo testcargo build --releaseYou can pull and run the pre-built multi-arch Docker image directly from GHCR:
docker run -d \
-p 8080:8080 \
-v /var/lib/fas-data:/data \
-e FAS_DATA_FILE=/data/fas.jsonl \
-e FAS_ACL_FILE=/data/acl.yaml \
--name fas \
ghcr.io/winguse/fas:latestIntegrate FAS as a ForwardAuth middleware in your Traefik router setup.
# YAML dynamic configuration
http:
middlewares:
fas-auth:
forwardAuth:
address: http://fas:8080/_auth
trustForwardHeader: true
authResponseHeaders:
- "Set-Cookie"Attach the middleware to any router that requires visitor approval.
http:
routers:
my-app-router:
rule: "Host(`app.example.com`)"
service: my-app-service
middlewares:
- fas-authIn Kubernetes, other pods can reach the FAS admin portal and APIs directly via the cluster network. Set FAS_SHARED_SECRET and configure Traefik to inject X-Shared-Secret on every request to FAS. FAS rejects any request missing the header, so only traffic routed through Traefik can reach the admin UI.
FAS deployment environment variable:
env:
- name: FAS_SHARED_SECRET
value: "your-strong-random-secret"Traefik middleware to inject the header:
http:
middlewares:
fas-inject-secret:
headers:
customRequestHeaders:
X-Shared-Secret: "your-strong-random-secret"Apply both middlewares to the FAS router:
http:
routers:
fas-admin-router:
rule: "Host(`fas.example.com`)"
service: fas-service
middlewares:
- fas-inject-secret # injects X-Shared-Secret before FAS sees the requestNote:
/_authis always exempt from the shared secret check — Traefik's ForwardAuth calls that endpoint directly and does not need the header.
Set FAS_SHARED_SECRET and configure Traefik to inject X-Shared-Secret on every request to FAS (see Traefik Integration above). This ensures the admin portal and APIs are unreachable directly from within the cluster.
Keep the administrator interface / and APIs /api/* protected by restricting them to local networks, VPNs, or requiring mTLS certificate verification at the proxy layer.
If the admin dashboard is placed behind fas-auth, assign your session cookie an allowed rule via curl:
- Access the application in your browser to generate a session cookie, and copy your visitor ID from the pending approval page.
- Run a
curlcommand from your host machine to assign the✅ allow_allACL rule to your session ID:# If FAS_SHARED_SECRET is set, include the header: curl -X POST http://localhost:8080/api/users/<your-uuid>/rule \ -H "Content-Type: application/json" \ -H "X-Shared-Secret: your-strong-random-secret" \ -d '{"acl_rule": "✅ allow_all"}'
- Refresh your browser page. Your session is now authorized, and you can manage visitors and ACL rules from the Admin dashboard.