mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-03-26 19:03:08 +00:00
security: add default security headers
This commit is contained in:
parent
d381478b29
commit
51a78b1eb4
3 changed files with 136 additions and 8 deletions
|
|
@ -55,6 +55,101 @@ async fn main() {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod security_headers_tests {
|
||||
use super::add_security_headers;
|
||||
use axum::body::Body;
|
||||
use axum::http::{header, Request, StatusCode};
|
||||
use axum::middleware;
|
||||
use axum::response::Response;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn security_headers_are_added_by_default() {
|
||||
let app = Router::new()
|
||||
.route("/api/ping", get(|| async { "ok" }))
|
||||
.layer(middleware::map_response(add_security_headers));
|
||||
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/ping")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let res = app.oneshot(req).await.unwrap();
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
assert_eq!(
|
||||
res.headers()
|
||||
.get("x-content-type-options")
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("nosniff")
|
||||
);
|
||||
assert_eq!(
|
||||
res.headers()
|
||||
.get("x-frame-options")
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("DENY")
|
||||
);
|
||||
assert_eq!(
|
||||
res.headers()
|
||||
.get("referrer-policy")
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("no-referrer")
|
||||
);
|
||||
assert_eq!(
|
||||
res.headers()
|
||||
.get("permissions-policy")
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("camera=(), microphone=(), geolocation=()")
|
||||
);
|
||||
assert_eq!(
|
||||
res.headers()
|
||||
.get("x-permitted-cross-domain-policies")
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("none")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn security_headers_do_not_override_explicit_headers() {
|
||||
let app = Router::new()
|
||||
.route(
|
||||
"/api/ping",
|
||||
get(|| async {
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("x-frame-options", "SAMEORIGIN")
|
||||
.header(header::REFERRER_POLICY, "strict-origin")
|
||||
.body(Body::from("ok"))
|
||||
.unwrap()
|
||||
}),
|
||||
)
|
||||
.layer(middleware::map_response(add_security_headers));
|
||||
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/ping")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let res = app.oneshot(req).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
res.headers()
|
||||
.get("x-frame-options")
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("SAMEORIGIN")
|
||||
);
|
||||
assert_eq!(
|
||||
res.headers()
|
||||
.get(header::REFERRER_POLICY)
|
||||
.and_then(|v| v.to_str().ok()),
|
||||
Some("strict-origin")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn run() -> Result<(), StartupError> {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
|
|
@ -349,6 +444,20 @@ async fn add_security_headers(mut res: Response) -> Response {
|
|||
);
|
||||
}
|
||||
|
||||
if !headers.contains_key("permissions-policy") {
|
||||
headers.insert(
|
||||
HeaderName::from_static("permissions-policy"),
|
||||
HeaderValue::from_static("camera=(), microphone=(), geolocation=()"),
|
||||
);
|
||||
}
|
||||
|
||||
if !headers.contains_key("x-permitted-cross-domain-policies") {
|
||||
headers.insert(
|
||||
HeaderName::from_static("x-permitted-cross-domain-policies"),
|
||||
HeaderValue::from_static("none"),
|
||||
);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ cp compose/.env.production.example compose/.env.production
|
|||
```
|
||||
|
||||
Required settings:
|
||||
|
||||
- `POSTGRES_PASSWORD` - Strong database password
|
||||
- `JWT_SECRET` - Random 64+ character string
|
||||
|
||||
|
|
@ -38,8 +39,8 @@ podman-compose --env-file .env.production -f production.yml up -d
|
|||
|
||||
### 4. Access
|
||||
|
||||
- Frontend: http://localhost:4321
|
||||
- Backend API: http://localhost:3000
|
||||
- Frontend: <http://localhost:4321>
|
||||
- Backend API: <http://localhost:3000>
|
||||
|
||||
## Manual Installation
|
||||
|
||||
|
|
@ -76,19 +77,21 @@ node ./dist/server/entry.mjs
|
|||
|
||||
## Configuration Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `compose/production.yml` | Production container deployment |
|
||||
| `compose/demo.yml` | Demo instance deployment |
|
||||
| `compose/.env.production.example` | Environment template |
|
||||
| `backend/.env` | Backend configuration |
|
||||
|File|Purpose|
|
||||
|---|---|
|
||||
|`compose/production.yml`|Production container deployment|
|
||||
|`compose/demo.yml`|Demo instance deployment|
|
||||
|`compose/.env.production.example`|Environment template|
|
||||
|`backend/.env`|Backend configuration|
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
For production, use a reverse proxy (nginx, Caddy) with:
|
||||
|
||||
- HTTPS termination
|
||||
- WebSocket support (for real-time features)
|
||||
- Proper headers
|
||||
- HSTS (set on the reverse proxy)
|
||||
|
||||
Example nginx config:
|
||||
|
||||
|
|
@ -107,12 +110,16 @@ server {
|
|||
proxy_set_header Connection "upgrade";
|
||||
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;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -33,6 +33,18 @@ Always use HTTPS in production:
|
|||
- Configure reverse proxy for TLS termination
|
||||
- Enable HSTS headers
|
||||
|
||||
### Security Headers
|
||||
|
||||
The backend sets a small set of security headers on responses by default:
|
||||
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
- `X-Frame-Options: DENY`
|
||||
- `Referrer-Policy: no-referrer`
|
||||
- `Permissions-Policy: camera=(), microphone=(), geolocation=()`
|
||||
- `X-Permitted-Cross-Domain-Policies: none`
|
||||
|
||||
Set `Strict-Transport-Security` (HSTS) on your reverse proxy, because the backend does not know whether requests arrived via HTTPS.
|
||||
|
||||
### CORS
|
||||
|
||||
Restrict CORS in production:
|
||||
|
|
|
|||
Loading…
Reference in a new issue