Skip to content

Commit 435169c

Browse files
committed
Security hardening: CSP, OIDC validation, API key hashing, audit logging
Add Content-Security-Policy header to all responses, tuned for iframe-based dashboard architecture. Validate OIDC ID tokens (signature, nonce, issuer, audience, expiry) using coreos/go-oidc. Replace plaintext API key storage with bcrypt hashing. Add structured audit logging (source=audit) for all security-relevant events across 5 files (21 audit points). Also: - Raise test coverage threshold to 85% (Go + frontend) - Add coverage enforcement to pre-push hook for frontend - Add vitest coverage thresholds (statements/lines 85%, branches 70%, functions 80%) - Update Codecov targets from 70%/auto to 85%/85% - Add OWASP ASVS Level 2 badge to README and wiki - Add security wiki page documenting all ASVS controls - Update wiki docs for api_key_hash (replaces plaintext api_key)
1 parent 23efb50 commit 435169c

File tree

24 files changed

+382
-177
lines changed

24 files changed

+382
-177
lines changed

.githooks/pre-push

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@ BOLD='\033[1m'
1717
NC='\033[0m'
1818

1919
# Coverage thresholds (percentage)
20-
GO_COVERAGE_THRESHOLD=80
21-
# Frontend lib/ coverage is what matters (components are 0% — that's normal for Svelte)
22-
# We only enforce Go coverage since SonarCloud gates on that
20+
GO_COVERAGE_THRESHOLD=85
21+
FE_COVERAGE_THRESHOLD=85
2322

2423
REPO_ROOT="$(git rev-parse --show-toplevel)"
2524

@@ -63,12 +62,31 @@ else
6362
fi
6463
rm -f "$GO_COVER_FILE"
6564

66-
# ─── Frontend tests ────────────────────────────────────────────────
67-
info "Running frontend tests..."
65+
# ─── Frontend tests with coverage ─────────────────────────────────
66+
info "Running frontend tests with coverage..."
6867

69-
if (cd "$REPO_ROOT/web" && npm run test 2>&1 | tail -20); then
68+
FE_OUTPUT=$(cd "$REPO_ROOT/web" && npx vitest run --coverage 2>&1)
69+
FE_EXIT=$?
70+
71+
if [ $FE_EXIT -eq 0 ]; then
7072
pass "Frontend tests passed"
73+
74+
# Parse overall statement coverage from the "All files" line
75+
FE_COVERAGE=$(echo "$FE_OUTPUT" | grep "All files" | awk -F'|' '{gsub(/ /,"",$2); print $2}')
76+
77+
if [ -n "$FE_COVERAGE" ]; then
78+
FE_COV_INT=$(echo "$FE_COVERAGE" | cut -d. -f1)
79+
if [ "$FE_COV_INT" -ge "$FE_COVERAGE_THRESHOLD" ]; then
80+
pass "Frontend coverage: ${FE_COVERAGE}% (threshold: ${FE_COVERAGE_THRESHOLD}%)"
81+
else
82+
fail "Frontend coverage: ${FE_COVERAGE}% — below ${FE_COVERAGE_THRESHOLD}% threshold"
83+
errors=$((errors + 1))
84+
fi
85+
else
86+
warn "Could not parse frontend coverage"
87+
fi
7188
else
89+
echo "$FE_OUTPUT" | tail -20
7290
fail "Frontend tests failed"
7391
errors=$((errors + 1))
7492
fi

CONTRIBUTING.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,33 @@ go build -tags embed_web -o muximux ./cmd/muximux
7171

7272
## Testing
7373

74+
### Coverage Target
75+
76+
Both backend (Go) and frontend (Svelte/TypeScript) maintain **85% statement/line coverage**. This is enforced by:
77+
78+
- **Pre-push hook** -- blocks pushes below threshold
79+
- **Codecov** -- gates PRs at 85% on new code
80+
- **Vitest** -- fails `npm run test:coverage` if coverage drops below threshold
81+
82+
Tests themselves are excluded from coverage metrics.
83+
7484
### Backend
7585

7686
```bash
7787
go test -race ./...
88+
89+
# With coverage report
90+
go test -race -coverprofile=coverage.txt ./internal/...
91+
go tool cover -func=coverage.txt | tail -1
7892
```
7993

8094
### Frontend
8195

8296
```bash
8397
cd web && npm run test
98+
99+
# With coverage report
100+
cd web && npx vitest run --coverage
84101
```
85102

86103
### Full pre-push check (mirrors CI)

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ One binary. One port. One YAML config file.
1515
[![Maintainability](https://sonarcloud.io/api/project_badges/measure?project=mescon_Muximux&metric=sqale_rating)](https://sonarcloud.io/summary/overall?id=mescon_Muximux)
1616
[![Security](https://sonarcloud.io/api/project_badges/measure?project=mescon_Muximux&metric=security_rating)](https://sonarcloud.io/summary/overall?id=mescon_Muximux)
1717
[![Codecov](https://codecov.io/gh/mescon/Muximux/branch/main/graph/badge.svg)](https://codecov.io/gh/mescon/Muximux)
18+
[![OWASP ASVS](https://img.shields.io/badge/OWASP_ASVS-Level_2-005571?logo=owasp)](docs/wiki/security.md)
1819
[![Docker](https://img.shields.io/badge/ghcr.io-mescon%2Fmuximux-blue?logo=docker)](https://ghcr.io/mescon/muximux)
1920
[![Release](https://img.shields.io/github/v/release/mescon/Muximux)](https://github.com/mescon/Muximux/releases/latest)
2021
![License](https://img.shields.io/badge/License-GPL%20v2-blue)
@@ -149,6 +150,7 @@ Full documentation is available in the **[Wiki](docs/wiki/README.md)**:
149150
- [Configuration Reference](docs/wiki/configuration.md) - All config.yaml options
150151
- [Apps](docs/wiki/apps.md) - Adding and configuring applications
151152
- [Built-in Reverse Proxy](docs/wiki/reverse-proxy.md) - How the proxy works and when to use it
153+
- [Security](docs/wiki/security.md) - Security measures and OWASP ASVS compliance
152154
- [Authentication](docs/wiki/authentication.md) - Auth methods and setup
153155
- [TLS & HTTPS](docs/wiki/tls-and-gateway.md) - Certificates and gateway mode
154156
- [Deployment Guide](docs/wiki/deployment.md) - Production deployment examples

cmd/muximux/main.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,6 @@ func main() {
7575
log.Fatalf("Failed to load configuration: %v", err)
7676
}
7777

78-
if cfg.Migrate() {
79-
if err := cfg.Save(*configPath); err != nil {
80-
log.Printf("Warning: failed to save migrated config: %v", err)
81-
}
82-
}
83-
8478
logFile := filepath.Join(*dataDir, "muximux.log")
8579
if err := logging.Init(logging.Config{
8680
Level: logging.Level(cfg.Server.LogLevel),
@@ -93,6 +87,11 @@ func main() {
9387

9488
applyOverrides(cfg, *listenAddr, *basePath)
9589

90+
// Warn if OIDC client_secret is stored as plaintext in config
91+
if cfg.Auth.OIDC.Enabled && cfg.Auth.OIDC.ClientSecret != "" && !config.IsBracedEnvRef(cfg.Auth.OIDC.ClientSecret) {
92+
logging.Warn("OIDC client_secret is stored in plaintext config — consider using ${ENV_VAR} syntax", "source", "config")
93+
}
94+
9695
// Create and start server
9796
srv, err := server.New(cfg, *configPath, *dataDir, version, commit, buildDate)
9897
if err != nil {

codecov.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ coverage:
22
status:
33
project:
44
default:
5-
target: auto
5+
target: 85%
66
patch:
77
default:
8-
target: 70%
8+
target: 85%
99

1010
flags:
1111
backend:

docs/wiki/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Muximux Wiki
22

3+
[![OWASP ASVS](https://img.shields.io/badge/OWASP_ASVS-Level_2-005571?logo=owasp)](security.md)
4+
[![Security](https://sonarcloud.io/api/project_badges/measure?project=mescon_Muximux&metric=security_rating)](https://sonarcloud.io/summary/overall?id=mescon_Muximux)
5+
[![Codecov](https://codecov.io/gh/mescon/Muximux/branch/main/graph/badge.svg)](https://codecov.io/gh/mescon/Muximux)
6+
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=mescon_Muximux&metric=alert_status)](https://sonarcloud.io/summary/overall?id=mescon_Muximux)
7+
38
Muximux is a modern, self-hosted web application portal for your homelab. It runs as a single binary, serves on a single port, and stores all configuration in one YAML file. Add your self-hosted applications, organize them into groups, and access them from a unified dashboard with health monitoring, keyboard shortcuts, and a built-in reverse proxy.
49

510
![Muximux dashboard](https://raw.githubusercontent.com/mescon/Muximux/main/docs/screenshots/09-dashboard-dark.png)
@@ -21,6 +26,7 @@ Muximux is designed to fit your setup, from a simple dashboard to a full reverse
2126
- [Configuration Reference](configuration.md) -- Full config.yaml format and all available options
2227
- [Apps](apps.md) -- Adding, configuring, and managing applications
2328
- [Built-in Reverse Proxy](reverse-proxy.md) -- Proxying app traffic through Muximux
29+
- [Security Overview](security.md) -- Security measures, OWASP ASVS compliance, and hardening details
2430
- [Authentication](authentication.md) -- Built-in auth, forward auth, and OIDC
2531
- [TLS & HTTPS](tls-and-gateway.md) -- Automatic certificates, custom certificates, and gateway mode
2632
- [Gateway Examples](gateway-examples.md) -- Recipes for proxying common homelab services

docs/wiki/_Sidebar.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- [Icons](icons)
1919

2020
**Security**
21+
- [Security Overview](security)
2122
- [Authentication](authentication)
2223
- [TLS & HTTPS](tls-and-gateway)
2324
- [Gateway Examples](gateway-examples)

docs/wiki/api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ GET /api/apps
7777
X-Api-Key: your-api-key-here
7878
```
7979

80-
The API key is configured in `auth.api_key` in your config file.
80+
The API key is configured as a bcrypt hash in `auth.api_key_hash` in your config file. See [Authentication > API Key Authentication](authentication.md#api-key-authentication) for setup instructions.
8181

8282
### User Management
8383

docs/wiki/authentication.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ auth:
3535
method: builtin
3636
session_max_age: 24h # How long sessions last (default: 24h)
3737
secure_cookies: true # Set true if serving over HTTPS
38-
api_key: "your-secret-key" # Optional: for API access without login
38+
api_key_hash: "$2a$10$..." # Optional: bcrypt hash of API key (see below)
3939
users:
4040
- username: admin
4141
password_hash: "$2a$10$..." # bcrypt hash
@@ -225,13 +225,44 @@ client_secret: ${OIDC_CLIENT_SECRET}
225225

226226
## API Key Authentication
227227

228-
When `api_key` is set in the auth config, you can authenticate API requests using the `X-Api-Key` header instead of a session cookie. This is useful for integrations, scripts, and automated tools.
228+
When `api_key_hash` is set in the auth config, you can authenticate API requests using the `X-Api-Key` header instead of a session cookie. This is useful for integrations, scripts, and automated tools.
229229

230230
```bash
231231
curl -H "X-Api-Key: your-secret-key" https://muximux.example.com/api/apps
232232
```
233233

234-
The API key is checked using constant-time comparison to prevent timing attacks.
234+
### How It Works
235+
236+
The API key is stored as a **bcrypt hash** in `config.yaml` -- not as plaintext. When a request arrives with `X-Api-Key`, Muximux verifies it against the stored hash using `bcrypt.CompareHashAndPassword`. This means:
237+
238+
- The original API key cannot be recovered from the config file
239+
- If `config.yaml` is compromised, the attacker cannot extract the key
240+
- Verification is constant-time, preventing timing attacks
241+
242+
### Generating an API Key Hash
243+
244+
Use the same tools as for password hashes:
245+
246+
```bash
247+
# Using the hashpw utility
248+
./hashpw 'my-api-key'
249+
250+
# Using htpasswd
251+
htpasswd -nbBC 10 "" 'my-api-key' | cut -d: -f2
252+
253+
# Using Python
254+
python3 -c "import bcrypt; print(bcrypt.hashpw(b'my-api-key', bcrypt.gensalt()).decode())"
255+
```
256+
257+
Then add the hash to your config:
258+
259+
```yaml
260+
auth:
261+
method: builtin
262+
api_key_hash: "$2a$10$..."
263+
```
264+
265+
You can also set the API key through the **Settings > Security** panel in the Muximux UI. The UI hashes the key automatically before storing it.
235266

236267
---
237268

docs/wiki/configuration.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ auth:
4040

4141
session_max_age: 24h # Session duration (default: 24h)
4242
secure_cookies: false # Require HTTPS for session cookies
43-
api_key: "" # Optional API key for X-Api-Key auth
43+
api_key_hash: "" # Optional bcrypt hash of API key for X-Api-Key auth
4444

4545
# Builtin auth users
4646
users:
@@ -180,7 +180,7 @@ Use `${VARIABLE_NAME}` in any string value to reference environment variables. T
180180
auth:
181181
oidc:
182182
client_secret: ${OIDC_CLIENT_SECRET}
183-
api_key: ${MUXIMUX_API_KEY}
183+
api_key_hash: ${MUXIMUX_API_KEY_HASH}
184184

185185
apps:
186186
- name: Plex
@@ -225,7 +225,7 @@ Everything else -- navigation, themes, apps, groups, icons, keybindings, health
225225

226226
You can export your configuration as a downloadable YAML file and import it on another instance:
227227

228-
- **Export** strips sensitive data (password hashes, OIDC secrets, API keys) before download.
228+
- **Export** strips sensitive data (password hashes, OIDC secrets, API key hashes) before download.
229229
- **Import** parses the uploaded YAML, validates it, and shows a preview so you can review before applying.
230230

231231
This is available in the Settings panel under the General tab, or via the API (see [API Reference](api.md)).

0 commit comments

Comments
 (0)