mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-03-26 19:03:08 +00:00
security: configurable CORS allowlist
This commit is contained in:
parent
aa2e7894b4
commit
4b4e2458e4
5 changed files with 180 additions and 7 deletions
|
|
@ -8,6 +8,15 @@ DATABASE_URL=postgres://likwid:likwid@localhost:5432/likwid
|
|||
SERVER_HOST=127.0.0.1
|
||||
SERVER_PORT=3000
|
||||
|
||||
# =============================================================================
|
||||
# CORS
|
||||
# =============================================================================
|
||||
# In development, CORS defaults to allowing any origin.
|
||||
# In production, set an allowlist (comma-separated) to restrict browser access.
|
||||
# Example:
|
||||
# CORS_ALLOWED_ORIGINS=https://openlikwid.org,https://staging.openlikwid.org
|
||||
CORS_ALLOWED_ORIGINS=
|
||||
|
||||
# JWT Secret for authentication tokens
|
||||
# IMPORTANT: Change this in production!
|
||||
JWT_SECRET=change-me-in-production
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ pub struct Config {
|
|||
pub server_host: String,
|
||||
#[serde(default = "default_server_port")]
|
||||
pub server_port: u16,
|
||||
#[serde(default)]
|
||||
pub cors_allowed_origins: Option<String>,
|
||||
/// Enable demo mode - restricts destructive actions and enables demo accounts
|
||||
#[serde(default)]
|
||||
pub demo_mode: bool,
|
||||
|
|
@ -74,6 +76,7 @@ impl Default for Config {
|
|||
database_url: "postgres://likwid:likwid@localhost:5432/likwid".to_string(),
|
||||
server_host: "127.0.0.1".to_string(),
|
||||
server_port: 3000,
|
||||
cors_allowed_origins: None,
|
||||
demo_mode: false,
|
||||
jwt_secret: default_jwt_secret(),
|
||||
rate_limit_enabled: default_rate_limit_enabled(),
|
||||
|
|
|
|||
|
|
@ -152,10 +152,7 @@ async fn run() -> Result<(), StartupError> {
|
|||
}
|
||||
}
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any);
|
||||
let cors = build_cors_layer(config.as_ref());
|
||||
|
||||
let plugins = plugins::PluginManager::new(pool.clone())
|
||||
.register_builtin_plugins()
|
||||
|
|
@ -354,3 +351,135 @@ async fn add_security_headers(mut res: Response) -> Response {
|
|||
|
||||
res
|
||||
}
|
||||
|
||||
fn build_cors_layer(config: &Config) -> CorsLayer {
|
||||
let layer = CorsLayer::new().allow_methods(Any).allow_headers(Any);
|
||||
|
||||
let allowed = config
|
||||
.cors_allowed_origins
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|v| !v.is_empty());
|
||||
|
||||
let Some(allowed) = allowed else {
|
||||
return layer.allow_origin(Any);
|
||||
};
|
||||
|
||||
let origins: Vec<HeaderValue> = allowed
|
||||
.split(',')
|
||||
.map(|v| v.trim())
|
||||
.filter(|v| !v.is_empty())
|
||||
.filter_map(|v| match HeaderValue::from_str(v) {
|
||||
Ok(hv) => Some(hv),
|
||||
Err(e) => {
|
||||
tracing::warn!(origin = v, error = %e, "Invalid CORS origin; ignoring");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if origins.is_empty() {
|
||||
return layer.allow_origin(Any);
|
||||
}
|
||||
|
||||
layer.allow_origin(origins)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod cors_tests {
|
||||
use super::build_cors_layer;
|
||||
use axum::body::Body;
|
||||
use axum::http::{header, Request, StatusCode};
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use tower::ServiceExt;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
#[tokio::test]
|
||||
async fn cors_default_allows_any_origin() {
|
||||
let cfg = Config::default();
|
||||
let app = Router::new()
|
||||
.route("/api/ping", get(|| async { "ok" }))
|
||||
.layer(build_cors_layer(&cfg));
|
||||
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/ping")
|
||||
.header(header::ORIGIN, "https://example.com")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let res = app.oneshot(req).await.unwrap();
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
res.headers()
|
||||
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("*")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cors_allowlist_only_allows_listed_origins() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.cors_allowed_origins = Some("https://a.example, https://b.example".to_string());
|
||||
let app = Router::new()
|
||||
.route("/api/ping", get(|| async { "ok" }))
|
||||
.layer(build_cors_layer(&cfg));
|
||||
|
||||
let allowed_req = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/ping")
|
||||
.header(header::ORIGIN, "https://a.example")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let allowed_res = app.clone().oneshot(allowed_req).await.unwrap();
|
||||
assert_eq!(allowed_res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
allowed_res
|
||||
.headers()
|
||||
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("https://a.example")
|
||||
);
|
||||
|
||||
let denied_req = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/ping")
|
||||
.header(header::ORIGIN, "https://c.example")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let denied_res = app.clone().oneshot(denied_req).await.unwrap();
|
||||
assert_eq!(denied_res.status(), StatusCode::OK);
|
||||
assert!(denied_res
|
||||
.headers()
|
||||
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cors_preflight_responds_for_allowed_origin() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.cors_allowed_origins = Some("https://a.example".to_string());
|
||||
let app = Router::new()
|
||||
.route("/api/ping", get(|| async { "ok" }))
|
||||
.layer(build_cors_layer(&cfg));
|
||||
|
||||
let req = Request::builder()
|
||||
.method("OPTIONS")
|
||||
.uri("/api/ping")
|
||||
.header(header::ORIGIN, "https://a.example")
|
||||
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let res = app.oneshot(req).await.unwrap();
|
||||
assert!(res.status().is_success());
|
||||
assert_eq!(
|
||||
res.headers()
|
||||
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("https://a.example")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ Likwid is configured through environment variables and database settings.
|
|||
| `SERVER_HOST` | No | `127.0.0.1` | Bind address |
|
||||
| `SERVER_PORT` | No | `3000` | HTTP port |
|
||||
| `DEMO_MODE` | No | `false` | Enable demo features |
|
||||
| `CORS_ALLOWED_ORIGINS` | No | - | Comma-separated allowlist of CORS origins for browsers (recommended in production) |
|
||||
| `RATE_LIMIT_ENABLED` | No | `true` | Enable API rate limiting |
|
||||
| `RATE_LIMIT_IP_RPM` | No | `300` | Requests per minute per IP |
|
||||
| `RATE_LIMIT_USER_RPM` | No | `1200` | Requests per minute per authenticated user |
|
||||
|
|
@ -118,12 +119,18 @@ Disabling:
|
|||
|
||||
### CORS
|
||||
|
||||
By default, CORS allows all origins in development. For production:
|
||||
By default, CORS allows any origin (development-friendly). In production, set `CORS_ALLOWED_ORIGINS` to a comma-separated allowlist.
|
||||
|
||||
```bash
|
||||
CORS_ALLOWED_ORIGINS=https://likwid.example.org
|
||||
```
|
||||
|
||||
Multiple origins:
|
||||
|
||||
```bash
|
||||
CORS_ALLOWED_ORIGINS=https://openlikwid.org,https://staging.openlikwid.org
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
### Log Levels
|
||||
|
|
|
|||
|
|
@ -5,36 +5,52 @@ Securing your Likwid instance.
|
|||
## Authentication
|
||||
|
||||
### JWT Tokens
|
||||
|
||||
- Use a strong, random `JWT_SECRET` (64+ characters)
|
||||
- Tokens expire after 24 hours by default
|
||||
- Refresh tokens are not stored server-side
|
||||
|
||||
### Password Policy
|
||||
|
||||
- Minimum 8 characters (configurable)
|
||||
- Bcrypt hashing with cost factor 12
|
||||
- No password in logs or error messages
|
||||
|
||||
### Two-Factor Authentication
|
||||
|
||||
Enable 2FA support for users:
|
||||
|
||||
- TOTP (Time-based One-Time Password)
|
||||
- Backup codes for recovery
|
||||
|
||||
## Network Security
|
||||
|
||||
### HTTPS
|
||||
|
||||
Always use HTTPS in production:
|
||||
|
||||
- Obtain certificates (Let's Encrypt recommended)
|
||||
- Configure reverse proxy for TLS termination
|
||||
- Enable HSTS headers
|
||||
|
||||
### CORS
|
||||
|
||||
Restrict CORS in production:
|
||||
```
|
||||
|
||||
```bash
|
||||
CORS_ALLOWED_ORIGINS=https://likwid.example.org
|
||||
```
|
||||
|
||||
For multiple origins, use a comma-separated list:
|
||||
|
||||
```bash
|
||||
CORS_ALLOWED_ORIGINS=https://openlikwid.org,https://staging.openlikwid.org
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Protect against abuse:
|
||||
|
||||
- 300 requests/minute per IP (default)
|
||||
- 1200 requests/minute per authenticated user
|
||||
- 30 requests/minute per IP for auth endpoints
|
||||
|
|
@ -42,11 +58,13 @@ Protect against abuse:
|
|||
## Database Security
|
||||
|
||||
### Connection
|
||||
|
||||
- Use SSL for database connections
|
||||
- Dedicated database user with minimal privileges
|
||||
- Strong, unique password
|
||||
|
||||
### Backups
|
||||
|
||||
- Regular automated backups
|
||||
- Encrypted backup storage
|
||||
- Test restore procedures
|
||||
|
|
@ -54,16 +72,20 @@ Protect against abuse:
|
|||
## API Security
|
||||
|
||||
### Input Validation
|
||||
|
||||
All inputs are validated:
|
||||
|
||||
- Type checking
|
||||
- Length limits
|
||||
- Sanitization
|
||||
|
||||
### SQL Injection
|
||||
|
||||
- Parameterized queries only (SQLx)
|
||||
- No raw SQL string concatenation
|
||||
|
||||
### XSS Prevention
|
||||
|
||||
- HTML escaping in templates
|
||||
- Content Security Policy headers
|
||||
- No inline scripts in production
|
||||
|
|
@ -71,6 +93,7 @@ All inputs are validated:
|
|||
## Moderation Audit Trail
|
||||
|
||||
All moderation actions are logged:
|
||||
|
||||
- Who performed the action
|
||||
- What action was taken
|
||||
- Why (reason required)
|
||||
|
|
@ -81,6 +104,7 @@ Logs are immutable and tamper-evident.
|
|||
## Updates
|
||||
|
||||
Keep Likwid updated:
|
||||
|
||||
- Watch the repository for security announcements
|
||||
- Apply patches promptly
|
||||
- Test updates in staging first
|
||||
|
|
@ -88,6 +112,7 @@ Keep Likwid updated:
|
|||
## Incident Response
|
||||
|
||||
If you discover a security issue:
|
||||
|
||||
1. Document the incident
|
||||
2. Assess impact
|
||||
3. Contain the breach
|
||||
|
|
@ -96,6 +121,6 @@ If you discover a security issue:
|
|||
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
Report security issues to: security@likwid.org
|
||||
Report security issues to: [security@likwid.org](mailto:security@likwid.org)
|
||||
|
||||
We follow responsible disclosure practices.
|
||||
|
|
|
|||
Loading…
Reference in a new issue