| Crates.io | runegate |
| lib.rs | runegate |
| version | 0.3.0 |
| created_at | 2025-05-29 17:07:08.772515+00 |
| updated_at | 2025-09-06 01:05:05.520538+00 |
| description | Lightweight Rust-based identity proxy |
| homepage | https://github.com/a1v0lut10n/runegate |
| repository | https://github.com/a1v0lut10n/runegate |
| max_upload_size | |
| id | 1694040 |
| size | 342,834 |
Runegate is a lightweight Rust-based identity proxy that enables secure, user-friendly access to internal or private web applications using magic email links.
It authenticates users without passwords by sending them time-limited login links, and forwards their requests to an internal service (such as a Gradio app) upon successful validation.
actix-web for high performancetracing for observabilityRUNEGATE_COOKIE_DOMAIN (defaults to host-only cookies)RUNEGATE_SESSION_COOKIE_NAME (default: runegate_id)/proxy/* maps to the target root path /*/debug/session, /debug/cookies, /debug/protectedDebug endpoints can now be toggled via the RUNEGATE_DEBUG_ENDPOINTS environment variable.
X-Runegate-Authenticated, X-Runegate-User, X-Forwarded-User, and X-Forwarded-Email for authenticated requests and strips any client-supplied versions of these headers.runegate/
βββ src/
β βββ main.rs # Main application with auth & proxy functionality
β βββ auth.rs # JWT token creation and validation
β βββ email.rs # Email configuration types
β βββ proxy.rs # Reverse proxy implementation
β βββ logging.rs # Structured logging and tracing setup
β βββ middleware.rs # Auth middleware implementation
β βββ send_magic_link.rs # Email sending functionality
β βββ memory_session_store.rs # In-memory session store shared across workers
β βββ rate_limit.rs # Rate limiting implementation
βββ static/
β βββ login.html # Login page for magic link requests
βββ examples/
β βββ send_email.rs # Example for testing email sending
β βββ test_target_service.rs # Demo service to proxy to
β βββ test_jwt_validation.rs # Tool for testing JWT tokens
βββ config/
β βββ email.toml # SMTP configuration
βββ docs/
β βββ architecture-overview.md # System design and deployment architecture
βββ .env # Optional: secrets and overrides
βββ Cargo.toml
βββ README.md
Additional documentation is available in the docs/ directory:
Runegate uses a reverse proxy architecture to secure access to your internal services:
βββββββββββββββ βββββββββββββββββ βββββββββββββββββββ
β β β β β β
β User ββββββββΊβ Runegate ββββββββΊβ Target Service β
β β β (7870) β β (7860) β
βββββββββββββββ βββββββββββββββββ βββββββββββββββββββ
Runegate Proxy (Port 7870): This is the service users directly access. It handles authentication and proxies requests to the target service.
Target Service (Port 7860): This is your internal application that Runegate protects. Users never access this directly, only through Runegate after authentication.
When a user clicks a magic link, they're directed to Runegate (port 7870), which validates their token, creates an authenticated session, and then proxies their requests to the target service (port 7860).
This separation keeps your internal service secure while Runegate handles all the authentication logic.
Deployment model: Prefer a dedicated subdomain (for example, app.example.com) that proxies all paths to Runegate. Path-based deployments (for example, example.com/app) are not supported by default and complicate cookie scoping and redirects.
When exposing Runegate on the internet, put nginx in front to terminate TLS and forward traffic to Runegate on 127.0.0.1:7870. Key requirements:
Host, X-Real-IP, X-Forwarded-For, set X-Forwarded-Proto https, and forward cookies.proxy_http_version 1.1.RUNEGATE_BASE_URL to the public HTTPS URL (e.g., https://app.example.com).RUNEGATE_COOKIE_DOMAIN unset for host-only cookies unless you require cross-subdomain scope.Reference nginx config:
# Port 80: redirect to HTTPS
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
# Port 443: TLS termination + reverse proxy
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://127.0.0.1:7870;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Cookie $http_cookie;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
client_max_body_size 10G;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_request_buffering off; # stream large uploads to upstream
proxy_redirect off;
proxy_buffering off;
}
}
Letβs Encrypt: If you use Certbot with the nginx authenticator, keep the port 80 server minimal. Certbot injects a temporary /.well-known/acme-challenge/ location during issuance/renewal.
Target service reachability: Ensure Runegate can reach your protected app (e.g., via WireGuard). For Gradio/Uvicorn, bind to 0.0.0.0 or the VPN IP and allow the VPS IP in your firewall.
Runegate can stream upstream responses to clients without buffering when RUNEGATE_STREAM_RESPONSES is enabled. This improves longβlived endpoints (upload progress, heartbeats) and very large downloads.
RUNEGATE_STREAM_RESPONSES=trueclient_max_body_size 10G;proxy_request_buffering off; (stream upload bodies to Runegate)proxy_buffering off; (avoid buffering responses at nginx)proxy_read_timeout 600s; proxy_send_timeout 600s;--proxy-headers --forwarded-allow-ips='*' so absolute URLs and scheme match the external origin.When disabled (default), Runegate buffers upstream responses before returning them. Enable streaming for better UX on progress/heartbeat endpoints and to support multiβGB downloads with lower memory usage.
Copy the example configuration file to set up your email settings:
# Copy the example config to create your own configuration
cp config/email.toml.example config/email.toml
# Edit the file with your credentials
editor config/email.toml
Then update config/email.toml with your Gmail SMTP credentials and message template:
smtp_host = "smtp.gmail.com"
smtp_port = 587
smtp_user = "your@gmail.com"
smtp_pass = "your_app_password"
from_address = "Runegate <your@gmail.com>"
subject = "Login Link"
body_template = """Click to log in:
{login_url}
This link is valid for 15 minutes."""
π‘ Use Gmail App Passwords with 2FA enabled for better security.
Configure your SMTP settings in config/email.toml
Start the application:
cargo run
The proxy will be available at http://localhost:7870
Magic links expire after a configurable period (set via RUNEGATE_MAGIC_LINK_EXPIRY environment variable). For applications requiring longer sessions, such as video editing or transcription tools, you can extend this period to accommodate extended user workflows without interruption.
Optional configuration through environment variables or .env file:
# Defines the operational environment (e.g., `development`, `production`).
# If set to `production`, stricter security rules are enforced:
# - RUNEGATE_JWT_SECRET and RUNEGATE_SESSION_KEY must be set.
# - RUNEGATE_SECURE_COOKIE defaults to `true`.
RUNEGATE_ENV=production
# JWT secret for token signing. Minimum 32 bytes recommended.
# Must be set if RUNEGATE_ENV=production (app will panic otherwise).
# If not set in non-production, a temporary secret is generated (unsafe for production).
RUNEGATE_JWT_SECRET=your_very_secure_random_string_for_jwt_at_least_32_bytes
# Session key for cookie encryption. Minimum 64 bytes required.
# Must be set if RUNEGATE_ENV=production (app will panic otherwise).
# If not set in non-production, a temporary key is generated (unsafe for production).
RUNEGATE_SESSION_KEY=your_very_secure_random_string_for_session_cookies_at_least_64_bytes
# Controls the `Secure` attribute of session cookies (`true` or `false`).
# If unset, defaults to `true` if RUNEGATE_ENV=production, otherwise `false`.
# Set to `true` when serving over HTTPS.
RUNEGATE_SECURE_COOKIE=true
# Optional: Cookie `Domain` attribute. If unset, a host-only cookie is used (recommended).
# Set only if you need the cookie to be sent to a specific parent domain.
# Example: app.example.com
RUNEGATE_COOKIE_DOMAIN=
# Optional: Session cookie name. Defaults to `runegate_id`.
# Change if the target app also uses a cookie named `id` or similar to avoid collisions.
RUNEGATE_SESSION_COOKIE_NAME=runegate_id
# Optional: Enable debug endpoints (/debug/session, /debug/cookies, /debug/protected)
# Defaults: disabled in production, enabled in development unless explicitly set.
RUNEGATE_DEBUG_ENDPOINTS=false
# Optional: Inject identity headers to the target service
# When enabled, Runegate injects X-Runegate-Authenticated, X-Runegate-User,
# X-Forwarded-User, and X-Forwarded-Email for authenticated requests.
# It also strips any client-supplied versions of these headers before forwarding.
# Default: true
RUNEGATE_IDENTITY_HEADERS=true
# Target service URL (defaults to http://127.0.0.1:7860)
RUNEGATE_TARGET_SERVICE=http://your-service-url
# Base URL for magic links (defaults to http://localhost:7870)
RUNEGATE_BASE_URL=https://your-public-url
# Magic link expiry time in minutes (defaults to 15)
RUNEGATE_MAGIC_LINK_EXPIRY=60 # Set longer for apps requiring extended sessions
# Rate limiting configuration
RUNEGATE_RATE_LIMIT_ENABLED=true # Enable/disable all rate limiting (default: true)
RUNEGATE_LOGIN_RATE_LIMIT=5 # Login attempts per minute per IP address (default: 5)
RUNEGATE_EMAIL_COOLDOWN=300 # Seconds between magic link requests per email (default: 300)
RUNEGATE_TOKEN_RATE_LIMIT=10 # Token verification attempts per minute per IP (default: 10)
# Logging level (defaults to runegate=debug,actix_web=info)
RUST_LOG=runegate=debug,actix_web=info,awc=debug
Runegate uses the tracing ecosystem for structured logging and observability:
# Run with default console logging (development mode)
cargo run
# Run with detailed debug logging
RUST_LOG=debug cargo run
# Run with very verbose tracing
RUST_LOG=debug,runegate=trace,actix_web=trace cargo run
Log levels can be configured for different components:
error: Only critical errorswarn: Warnings and errorsinfo: General information plus warnings/errors (default)debug: Detailed debugging informationtrace: Very verbose tracing informationExample log output patterns:
# HTTP requests are automatically logged with timing information
[INFO] runegate::middleware: User is authenticated, allowing access to: /dashboard
# Auth events are logged
[INFO] runegate::auth: Magic link generated for user@example.com
Runegate supports two logging formats:
The logging format can be configured using the RUNEGATE_LOG_FORMAT environment variable, which can be set in your .env file or directly in the environment. This eliminates the need to recompile when switching formats.
Runegate implements a multi-layered rate limiting system to protect against brute force attacks, abuse, and denial of service attempts. Three distinct rate limiting mechanisms work together to secure the authentication process:
Per-IP Login Rate Limiting: Caps the number of login attempts from a single IP address
Per-Email Cooldown: Enforces a cooldown period between magic link requests for the same email
Token Verification Rate Limiting: Restricts the number of token verification attempts per IP
When rate limits are exceeded, Runegate responds with:
Rate limiting can be configured via environment variables:
# Enable or disable all rate limiting (true/false)
RUNEGATE_RATE_LIMIT_ENABLED=true
# Number of login attempts allowed per minute per IP address
RUNEGATE_LOGIN_RATE_LIMIT=5
# Cooldown period in seconds between magic link requests per email
RUNEGATE_EMAIL_COOLDOWN=300
# Number of token verification attempts allowed per minute per IP
RUNEGATE_TOKEN_RATE_LIMIT=10
Runegate includes both unit tests and integration tests for rate limiting features:
# Test rate limiting components in isolation
cargo test --test rate_limit_tests
Automated testing scripts make it easy to test rate limiting against a running server:
# Test all rate limiting features
./scripts/run_integration_tests.sh
# Test only email cooldown feature
./scripts/run_integration_tests.sh test_email_cooldown_rate_limiting
# Test with rate limiting disabled
RUNEGATE_RATE_LIMIT_ENABLED=false ./scripts/run_integration_tests.sh test_rate_limit_disabled
You can also manually test rate limiting by making repeated requests to endpoints:
# Test login rate limiting
for i in {1..10}; do \
curl -X POST -H "Content-Type: application/json" \
-d '{"email":"test@example.com"}' \
http://localhost:7870/login; \
echo ""; \
done
# Test token verification rate limiting
for i in {1..15}; do \
curl http://localhost:7870/auth?token=invalid_token; \
echo ""; \
done
The rate limiting implementation uses:
All rate limiting state is stored in memory and will reset when the service restarts.
In your .env file:
# Console logging (default)
RUST_LOG=info
# Or JSON logging for production
RUST_LOG=info
RUNEGATE_LOG_FORMAT=json
Via environment variables:
# Run with console logging (default)
RUST_LOG=info cargo run
Runegate implements three types of rate limiting mechanisms to protect against abuse:
IP-based Login Rate Limiting - Prevents brute force login attempts by limiting the number of login requests from the same IP address.
X-RateLimit-Exceeded: IP and X-RateLimit-Reset: 60 headers included.Email-based Cooldown - Prevents sending multiple magic links to the same email address in quick succession.
X-RateLimit-Exceeded: Email and X-RateLimit-Reset: <remaining_seconds> headers included.Token Verification Rate Limiting - Limits attempts to verify auth tokens from the same IP address.
X-RateLimit-Exceeded: IP and X-RateLimit-Reset: 60 headers included.Configuring Rate Limiting:
Rate limits can be adjusted or disabled through environment variables:
# Enable or disable all rate limiting
RUNEGATE_RATE_LIMIT_ENABLED=true # Set to false to disable all rate limiting
# Configure limits
RUNEGATE_LOGIN_RATE_LIMIT=5 # Login attempts per minute per IP
RUNEGATE_EMAIL_COOLDOWN=300 # Seconds between magic links for same email
RUNEGATE_TOKEN_RATE_LIMIT=10 # Token verification attempts per minute
Testing Mode:
For development and testing, you can disable rate limiting entirely:
RUNEGATE_RATE_LIMIT_ENABLED=false
RUST_LOG=info RUNEGATE_LOG_FORMAT=json cargo run > runegate.log
# For Docker or other environments
export RUST_LOG=info
export RUNEGATE_LOG_FORMAT=json
cargo run
JSON logs can be easily processed by log aggregation tools like Elasticsearch, Grafana Loki, or other similar systems, and contain all the same contextual information as the console logs but in a machine-readable format.
Runegate includes several example scripts for testing:
# Test email sending
cargo run --example send_email -- recipient@example.com
# Generate a JWT token for testing
cargo run --example test_jwt_validation your@email.com
# Run a test target service
cargo run --example test_target_service
Runegate can be deployed as a systemd service on Debian-based systems with our automated installation system:
# Clone the repository
git clone https://github.com/a1v0lut10n/runegate.git
cd runegate
# Run the installation script (as root)
sudo ./deploy/install.sh
# Configure your environment
sudo nano /etc/runegate/runegate.env
sudo nano /etc/runegate/config/email.toml
# Start and enable the service
sudo systemctl start runegate
sudo systemctl enable runegate
The systemd deployment includes:
runegate userProtectSystem=strictSee the deployment guide for complete documentation.
Runegate is designed to sit behind a reverse proxy on a dedicated subdomain.
app.example.com, pointing to your VPS.git clone https://github.com/a1v0lut10n/runegate.git
cd runegate
sudo ./deploy/install.sh
sudo nano /etc/runegate/runegate.env
sudo nano /etc/runegate/config/email.toml
Recommended /etc/runegate/runegate.env for HTTPS deployments:
RUNEGATE_ENV=production
RUNEGATE_JWT_SECRET=... # >= 32 bytes
RUNEGATE_SESSION_KEY=... # >= 64 bytes
RUNEGATE_SECURE_COOKIE=true
RUNEGATE_BASE_URL=https://app.example.com
RUNEGATE_TARGET_SERVICE=http://127.0.0.1:7860
# Optional
# RUNEGATE_COOKIE_DOMAIN=app.example.com # or leave unset for host-only
RUNEGATE_SESSION_COOKIE_NAME=runegate_id
RUST_LOG=info
RUNEGATE_LOG_FORMAT=json
Start and enable:
sudo systemctl start runegate
sudo systemctl enable runegate
Example Nginx config to terminate TLS and proxy to Runegate:
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
# Optional: HSTS
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:7870;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 300s;
proxy_send_timeout 60s;
client_max_body_size 50m;
}
}
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
Notes:
location / that proxies to Runegate (no location /proxy/).RUNEGATE_BASE_URL in sync with the public URL.RUNEGATE_COOKIE_DOMAIN unset for host-only cookies unless a parent domain is needed.RUNEGATE_DEBUG_ENDPOINTS (recommended off in production).After deploy, verify:
curl -I https://app.example.com/health
curl https://app.example.com/rate_limit_info
Login flow:
/proxy/ and land on the target app.Troubleshooting endpoints (for temporary use):
https://app.example.com/debug/cookies β shows client cookies as seen by Runegate.https://app.example.com/debug/session β shows server-side session view.https://app.example.com/debug/protected β passes through auth middleware; returns 200 only if authenticated.Security tip: Restrict location /debug/ in Nginx to trusted IPs, or remove these routes after validation.
Deploying any application, including Runegate, requires careful attention to security. Here are some best practices to ensure your Runegate deployment is as secure as possible:
RUNEGATE_SECURE_COOKIE is set to true) and the tokens within magic links.RUNEGATE_JWT_SECRET (min 32 bytes) and RUNEGATE_SESSION_KEY (min 64 bytes) are cryptographically strong, unique random strings. Do not use default or easily guessable values..env files, ensuring the file has restrictive permissions (e.g., readable only by the runegate user).RUNEGATE_ENV=productionRUNEGATE_ENV=production in your production deployments.RUNEGATE_JWT_SECRET and RUNEGATE_SESSION_KEY are set, and defaults RUNEGATE_SECURE_COOKIE to true.runegate). The provided systemd unit example already does this.runegate.service file includes ProtectSystem=strict, which is a good starting point for systemd-based deployments. Review and apply other relevant hardening options.ufw, firewalld, or cloud provider firewalls) to only expose the port Runegate is listening on (typically 7870, or the port your HTTPS reverse proxy listens on, e.g., 443) to users or the internet.cargo update and rebuilding Runegate.cargo audit to check for known vulnerabilities in dependencies and update them as needed.RUNEGATE_LOG_FORMAT=json) is recommended for easier parsing and ingestion into log management systems.By following these best practices, you can significantly improve the security posture of your Runegate deployment.
[[routes]] blocks for services)@yourcompany.com)docker-compose.yml supportWhile no single crate offers the exact functionality of runegate β a lightweight Rust-based identity proxy using magic-link authentication over email β several existing projects and libraries provide composable building blocks that inspired or overlap with runegate's functionality.
runegate is implemented using a combination of:
lettre for SMTP email delivery,jsonwebtoken for secure, time-limited tokens,actix-web for handling HTTP routes,actix-session for session management, andgovernor for rate limiting.Below is a list of related crates that provide similar or complementary functionality.
oxide-auth β A full-featured OAuth2 server library for Rust. Good for token-based auth, but not tailored for email-based flows.jsonwebtoken β Essential for encoding and decoding secure JWTs with embedded claims such as expiry and user identity.actix-identity β Identity middleware for Actix. Useful for managing login state with cookies.lettre β A modern email library for sending SMTP messages securely over TLS or STARTTLS.uuid β For generating cryptographically random identifiers used in login links.rand β Provides secure token or nonce generation.actix-web β A powerful and performant web framework used to handle routing and request processing.warp β An alternative web framework with strong type safety and composability.axum-login β A login/session management layer built for Axum. Useful for session-based identity, though it uses password-based auth by default.auth0 β Integration support for Auth0, a commercial identity provider with passwordless login flows. Suitable for projects using SaaS identity infrastructure.These libraries can serve as a foundation for your own magic-link or token-based identity proxy solution if you are not using runegate.
7870 (local) or behind NginxRunegate is designed for simplicity and security when exposing private tools to trusted users β with no passwords to manage.
This project is licensed under the Apache License 2.0.
Earlier versions could appear to work behind a path (e.g., example.com/app), but reliable operation requires a dedicated subdomain due to cookie scope, redirects, and proxy prefix handling. The current design targets subdomain deployment by default.
Steps to migrate:
app.example.com pointing to your VPS.app.example.com with a single location / proxying to Runegate.RUNEGATE_BASE_URL=https://app.example.com.RUNEGATE_COOKIE_DOMAIN unset (host-only) unless you need a parent domain.RUNEGATE_TARGET_SERVICE=http://127.0.0.1:7860.location /app/) that previously attempted to βmountβ Runegate under a path.Note on path-based setups:
example.com/app) is not supported out-of-the-box. Supporting it would require a configurable base path for all routes, cookie path scoping, and adjusted proxy stripping logic. If you require this, open an issue β it can be added behind a feature flag, but subdomain routing is recommended for simplicity and reliability.When Runegate authenticates a user, you may want the downstream target service to know who the user is (e.g., to restore per-user state). There are two approaches:
RUNEGATE_IDENTITY_HEADERS=true (default true).X-Runegate-Authenticated: true|falseX-Runegate-User: <email>X-Forwarded-User: <email>X-Forwarded-Email: <email>X-Forwarded-User or X-Forwarded-Email to associate a request with a user.127.0.0.1:7860) so only Runegate can reach it.For stronger trust across multiple services, Runegate can inject a shortβlived JWT, signed with a dedicated keypair.
Authorization: Bearer <jwt> (or a custom header like X-Runegate-JWT).{
"sub": "user@example.com",
"email": "user@example.com",
"iat": 1710000000,
"exp": 1710000600,
"iss": "runegate",
"aud": "your-target",
"sid": "optional-session-id"
}
RS256 or EdDSA (Ed25519). Targets only need the public key.kid header; targets can fetch a JWKS or be provisioned with the new public key.Planned environment variables (subject to change):
# Select identity mode: headers | jwt | none
RUNEGATE_IDENTITY_MODE=jwt
# JWT algorithm: RS256 | EdDSA | HS256
RUNEGATE_DOWNSTREAM_JWT_ALG=RS256
# TTL (seconds) for downstream JWTs
RUNEGATE_DOWNSTREAM_JWT_TTL=600
# Issuer and audience
RUNEGATE_DOWNSTREAM_JWT_ISS=runegate
RUNEGATE_DOWNSTREAM_JWT_AUD=your-target
# Where to place the token
RUNEGATE_DOWNSTREAM_JWT_HEADER=Authorization
RUNEGATE_DOWNSTREAM_JWT_BEARER=true # prefix with "Bearer "
# Keying (choose one set based on the algorithm)
# RS256 / EdDSA (preferred): Runegate signs with private key; targets verify with public key
RUNEGATE_DOWNSTREAM_JWT_PRIVATE_KEY_PATH=/etc/runegate/keys/downstream_private.pem
# Optional inline alternative
# RUNEGATE_DOWNSTREAM_JWT_PRIVATE_KEY_BASE64=...
# HS256 (shared secret; simpler but less isolated trust)
# RUNEGATE_DOWNSTREAM_JWT_SECRET=your-very-strong-shared-secret
# Optional JWKS publication (if you want targets to fetch keys)
# RUNEGATE_DOWNSTREAM_JWKS_ENABLED=false
# RUNEGATE_DOWNSTREAM_JWKS_PATH=/jwks.json
Target verification sketch:
jwt.decode(token, public_key, algorithms=["RS256"], audience="your-target", issuer="runegate").jwtVerify(token, publicKey, { algorithms: ["EdDSA"], audience: "your-target", issuer: "runegate" }).Security notes:
sid claim for optional state binding.