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> {
|
async fn run() -> Result<(), StartupError> {
|
||||||
dotenvy::dotenv().ok();
|
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
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ cp compose/.env.production.example compose/.env.production
|
||||||
```
|
```
|
||||||
|
|
||||||
Required settings:
|
Required settings:
|
||||||
|
|
||||||
- `POSTGRES_PASSWORD` - Strong database password
|
- `POSTGRES_PASSWORD` - Strong database password
|
||||||
- `JWT_SECRET` - Random 64+ character string
|
- `JWT_SECRET` - Random 64+ character string
|
||||||
|
|
||||||
|
|
@ -38,8 +39,8 @@ podman-compose --env-file .env.production -f production.yml up -d
|
||||||
|
|
||||||
### 4. Access
|
### 4. Access
|
||||||
|
|
||||||
- Frontend: http://localhost:4321
|
- Frontend: <http://localhost:4321>
|
||||||
- Backend API: http://localhost:3000
|
- Backend API: <http://localhost:3000>
|
||||||
|
|
||||||
## Manual Installation
|
## Manual Installation
|
||||||
|
|
||||||
|
|
@ -77,7 +78,7 @@ node ./dist/server/entry.mjs
|
||||||
## Configuration Files
|
## Configuration Files
|
||||||
|
|
||||||
|File|Purpose|
|
|File|Purpose|
|
||||||
|------|---------|
|
|---|---|
|
||||||
|`compose/production.yml`|Production container deployment|
|
|`compose/production.yml`|Production container deployment|
|
||||||
|`compose/demo.yml`|Demo instance deployment|
|
|`compose/demo.yml`|Demo instance deployment|
|
||||||
|`compose/.env.production.example`|Environment template|
|
|`compose/.env.production.example`|Environment template|
|
||||||
|
|
@ -86,9 +87,11 @@ node ./dist/server/entry.mjs
|
||||||
## Reverse Proxy
|
## Reverse Proxy
|
||||||
|
|
||||||
For production, use a reverse proxy (nginx, Caddy) with:
|
For production, use a reverse proxy (nginx, Caddy) with:
|
||||||
|
|
||||||
- HTTPS termination
|
- HTTPS termination
|
||||||
- WebSocket support (for real-time features)
|
- WebSocket support (for real-time features)
|
||||||
- Proper headers
|
- Proper headers
|
||||||
|
- HSTS (set on the reverse proxy)
|
||||||
|
|
||||||
Example nginx config:
|
Example nginx config:
|
||||||
|
|
||||||
|
|
@ -107,12 +110,16 @@ server {
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
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 {
|
location /api {
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
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
|
- Configure reverse proxy for TLS termination
|
||||||
- Enable HSTS headers
|
- 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
|
### CORS
|
||||||
|
|
||||||
Restrict CORS in production:
|
Restrict CORS in production:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue