Admin panel for managing Traefik dynamic configurations with authentication support, including shared links and SSO integration.
Can be used standalone but built in mind with Headscale and other VPN to expose internally hosted servcices to the outside world. Enabling functionality in some ways similar to Tailscale Funnel but for Headscale. Inspired by the approach of Pangolin.
- Dynamic Traefik Configuration: Automatically generates Traefik configurations from database
- Service Management: Full CRUD operations for proxy services
- Multiple Authentication Methods:
- No authentication
- Shared links with expiry
- SSO integration with group/user authorization
- Session Management: Memory-cached sessions with admin oversight
- Real-time Updates: Live configuration updates for Traefik
- Modern UI: Built with Next.js 15, TypeScript, and shadcn/ui
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Admin Panel │────▶│ PostgreSQL │ │ Traefik │
│ (Next.js) │ │ Database │ │ Reverse │
└─────────────────┘ └─────────────────┘ │ Proxy │
└─────────────────┘
│
┌───────────────────────────┘
▼
┌─────────────────┐
│ Target Services │
│ (HTTP/HTTPS) │
└─────────────────┘
There are two ways to run this panel:
- Externally-managed Traefik (default) — you run and configure Traefik yourself; the panel only supplies the dynamic configuration via Traefik's HTTP provider. Everything below in Quick Start and Traefik Configuration describes this mode. It is the most flexible and powerful option.
- Fully-managed Traefik — a one-command docker compose bundle where the panel also owns Traefik's static configuration (entrypoints, ACME certificate resolvers) and Traefik restarts itself when you change them. See Fully-Managed Traefik Bundle.
The bundle (docker-compose.managed.yml) starts PostgreSQL, the admin panel
and Traefik together with sane defaults: web (:80, redirecting to https),
websecure (:443, default TLS) and a letsencrypt TLS-challenge resolver.
cp docker/managed/.env.example .env
# set POSTGRES_PASSWORD, ADMIN_PANEL_AUTH (htpasswd entry), MANAGED_SECRETS_KEY
# and ADMIN_PANEL_DOMAIN — see the comments in the file
docker compose -f docker-compose.managed.yml up -dHow it works:
- The panel serves Traefik's
traefik.ymlat/api/traefik/static-config. A small wrapper script inside the Traefik container fetches it on boot, polls it every 30 seconds, and restarts Traefik automatically when it changes (static config cannot be hot-reloaded). The config page shows whether Traefik has picked up the latest config yet. - Rollback: once Traefik has run a fetched config for ~20 s the wrapper
snapshots it as last-good under
/data/wrapperon the Traefik volume (the config only — credentials are never written to persistent storage). If Traefik exits before a newly applied config reaches that grace period, the wrapper logs a loud warning, restores last-good and restarts from it — and keeps ignoring that rejected config until the panel serves a different one or a five-minute cooldown expires (RETRY_SECONDSon the wrapper), allowing transient startup failures to recover automatically. Last-good is also used at boot when the panel is unreachable (before falling back to a minimal built-in config). Saves are validated server-side (types, known fields only, ACME email format, known lego DNS provider codes with a custom escape hatch, a TLS-enabled listener on container port 443 with no redirect), so the rollback is the safety net, not the first line of defense. - Admin access: container port 443 must remain a TLS listener because the supplied compose publishes only 80/443. Extra listeners are allowed, but moving the only TLS listener to 8443 is rejected. Plain HTTP-only managed configurations are unsupported, including behind an upstream TLS terminator. The admin router remains HTTPS-only and authenticated on Traefik's default listeners, following the running configuration even after entrypoint renames and rollback. Its high priority reserves the entire admin hostname: user services must use another hostname, including services with path rules. The minimal recovery config has no certificate resolver, so recovery HTTPS uses Traefik's default self-signed certificate and shows a browser warning. The config page flags a missing heartbeat after 90 seconds (three default wrapper polls); adjust this threshold if you customise the polling interval.
- Entrypoints and certificate resolvers are edited in the Managed Traefik
section on the config page. DNS-challenge resolvers also need their provider
credentials (e.g.
CF_DNS_API_TOKENfor Cloudflare); set them right there under DNS provider credentials — any number of them, for one or several resolvers/providers. Credentials are write-only: they can be set or replaced through the web but are never returned, and there is no HTTP endpoint that serves them — not to the wrapper, not to anyone. Values are stored encrypted at rest in a file on the panel volume (AES-256-GCM with the envelope header bound as authenticated data, keyed byMANAGED_SECRETS_KEY, at least 32 characters) — never in the database, which keeps only the names. To hand them to Traefik the panel decrypts them and writes a shell-sourceable env file (mode 0600) onto themanaged_secretsvolume, which compose backs with a tmpfs and mounts read-only into the Traefik container: the plaintext exists in RAM only, never on disk and never on the network. The wrapper polls that file, injects the variables as env, and restarts Traefik when they change (within its 30 s poll). After a host reboot the tmpfs is empty until the panel rewrites it on startup. The managed status reportssecretsEnv: { materialized, writtenAt, stale }. You can still hard-code credentials as env vars on thetraefikservice instead, if you prefer. - Rotating
MANAGED_SECRETS_KEYmakes the stored credentials unreadable — they are gone, not recoverable. The managed status then reportssecretsUndecryptable: true; saving credentials again withreset: true(or removing every stored name) discards the old file and starts fresh, after which you re-enter the values. Credential files written by pre-release builds (format v1, no authenticated header) are treated the same way. - Security model: only ports 80/443 are published. The panel (3000) and
the Traefik API (8080) stay on the internal compose network; the panel is
reachable only through an auto-generated Traefik route on your
Admin Panel Domain, protected by HTTP basic auth from
ADMIN_PANEL_AUTH(required — the bundle refuses to start without it). The wrapper fetches the static config (no secrets in it) over plain HTTP inside the compose network; credentials never travel over the network — they move through the in-memorymanaged_secretsvolume only, and the wrapper's working copy lives on atmpfs(/run) in the Traefik container.
First-time setup:
- In
.envsetADMIN_PANEL_DOMAINto the DNS name you will use for the panel (e.g.admin.example.com) and point that record at the host. In managed mode the panel seeds its Admin Panel Domain from this variable on first start — only while the stored value is still the default; a domain set later in the UI always wins. docker compose -f docker-compose.managed.yml up -d. Traefik starts with built-in defaults, fetches the managed config and publishes the admin route; the panel becomes available athttps://admin.example.combehind the basic-auth prompt within about a minute (the first certificate issuance takes a moment).- On the Configuration page set your ACME email in the Managed Traefik section (and any DNS-provider credentials). Traefik restarts within ~30 seconds.
git clone <repository-url>
cd traefik-proxy-admin
pnpm install# Start PostgreSQL with Docker Compose
docker-compose up -d
# Generate and run database migrations
pnpm db:generate
pnpm db:pushcp .env.example .env
# Edit .env with your configurationpnpm devThe admin panel will be available at http://localhost:3000
- Service configuration (name, subdomain, target IP/port)
- Authentication method (none, shared_link, sso)
- Enable/disable status
- SSO user/group authorization
- One-time or expiring shared links
- Session duration configuration
- Usage tracking
- Active user sessions
- Memory-cached for performance
- Automatic cleanup of expired sessions
- Application-wide configuration
- SSO provider settings
- Global domain and certificate configuration
- Global middleware settings
GET /api/traefik/config- Dynamic Traefik configuration
GET /api/services- List all servicesPOST /api/services- Create new servicePUT /api/services/[id]- Update serviceDELETE /api/services/[id]- Delete servicePOST /api/services/share-link- Generate shared link
GET /api/auth/verify- Forward-auth endpoint for TraefikPOST /api/auth/shared-link- Authenticate with shared linkGET /api/auth/sso/login- Initiate SSO loginGET /api/auth/sso/callback- SSO callback handler
GET /api/sessions- List active sessionsDELETE /api/sessions- Delete all sessionsDELETE /api/sessions/[id]- Delete specific session
GET /api/config- Get global Traefik configurationPUT /api/config- Update global configuration
The admin panel now supports configurable global settings that affect all services:
- Base Domain: Set the root domain (e.g.,
exposed.example.com) - Services become accessible as
{subdomain}.{baseDomain} - Supports wildcard certificates for privacy (no service names in CT logs)
- Cert Resolver: Configurable Traefik certificate resolver name
- Supports DNS challenge mode for wildcard certificates
- Example:
letsencrypt-dnsfor*.exposed.example.com
- Global Middlewares: Applied to all services automatically
- Per-Service Middlewares: Additional middlewares per service
- Order: Global → Auth (if enabled) → HTTPS redirect → Service-specific
Configure Traefik to use this service as a configuration provider:
# traefik.yml
providers:
http:
endpoints:
- "http://localhost:3000/api/traefik/config"
pollInterval: "10s"
# Forward authentication
api:
dashboard: true
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
# Configure your certificate resolver for wildcard certificates
certificatesResolvers:
letsencrypt-dns: # Match this name in admin panel
acme:
email: [email protected]
storage: acme.json
dnsChallenge:
provider: cloudflare # Your DNS provider
delayBeforeCheck: 10{
"certResolver": "letsencrypt-dns",
"globalMiddlewares": ["compression", "security-headers", "rate-limit"]
}This configuration will:
- Make services accessible as
{service}.exposed.example.com - Use wildcard certificate
*.exposed.example.com - Apply compression, security headers, and rate limiting to all services
Services are publicly accessible without any authentication.
- Generate time-limited, one-use links
- Configurable session duration
- Automatic session creation upon link usage
- Configurable OAuth2/OIDC providers
- Group and user-based authorization
- Automatic session management
SSO settings are managed through the admin panel and stored in the app_config table:
{
"enabled": true,
"idpUrl": "https://your-idp.com",
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
"redirectUri": "http://localhost:3000/api/auth/sso/callback",
"scopes": ["openid", "profile", "groups"]
}- Sessions are stored in PostgreSQL and cached in memory for performance
- Automatic cleanup of expired sessions
- Admin interface for viewing and managing active sessions
- Real-time session validation for Traefik forward-auth
# Generate new migration
pnpm db:generate
# Push schema changes
pnpm db:push
# View database in Drizzle Studio
pnpm db:studiosrc/
├── components/ui/ # shadcn/ui components
├── db/ # Database schema and connection
├── lib/ # Utility functions
│ ├── traefik-config.ts # Traefik configuration generation
│ ├── session-manager.ts # Session management with memory cache
│ ├── shared-links.ts # Shared link utilities
│ ├── sso-config.ts # SSO configuration and handlers
│ └── utils.ts # General utilities
app/
├── api/ # API routes
│ ├── services/ # Service CRUD operations
│ ├── sessions/ # Session management
│ ├── traefik/ # Traefik configuration endpoint
│ └── auth/ # Authentication endpoints
├── sessions/ # Session management UI
├── auth/ # Authentication pages
└── page.tsx # Main admin panel
- All authentication tokens are stored securely with httpOnly cookies
- CSRF protection through state parameters in SSO flows
- Session tokens are cryptographically secure random values
- Forward-auth validation prevents unauthorized access
- Automatic session cleanup prevents token accumulation
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
Licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later).
This means you are free to use, modify, and distribute this software, but if you run a modified version as a network service, you must make your source code available to its users. See the LICENSE file for the full text.



