Doctor plugin checks: implement health check classes and documentation

Implement remediation-aware health checks across all Doctor plugin modules
(Agent, Attestor, Auth, BinaryAnalysis, Compliance, Crypto, Environment,
EvidenceLocker, Notify, Observability, Operations, Policy, Postgres, Release,
Scanner, Storage, Vex) and their backing library counterparts (AI, Attestation,
Authority, Core, Cryptography, Database, Docker, Integration, Notify,
Observability, Security, ServiceGraph, Sources, Verification).

Each check now emits structured remediation metadata (severity, category,
runbook links, and fix suggestions) consumed by the Doctor dashboard
remediation panel.

Also adds:
- docs/doctor/articles/ knowledge base for check explanations
- Advisory AI search seed and allowlist updates for doctor content
- Sprint plan for doctor checks documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-27 12:28:00 +02:00
parent fbd24e71de
commit c58a236d70
326 changed files with 18500 additions and 463 deletions

View File

@@ -0,0 +1,83 @@
---
checkId: check.security.apikey
plugin: stellaops.doctor.security
severity: warn
tags: [security, apikey, authentication]
---
# API Key Security
## What It Checks
Validates API key configuration and security practices. The check only runs when an API key configuration section exists (`ApiKey`, `Authentication:ApiKey`, or `Security:ApiKey`). It inspects:
| Setting | Threshold/Condition | Issue |
|---|---|---|
| `MinLength` | Less than 32 characters | Key too short (escalates to `fail` if < 16) |
| `AllowInQueryString` | `true` | Keys in query strings get logged in access logs |
| `HeaderName` | Equals `Authorization` | Conflicts with other auth schemes |
| `RateLimitPerKey` | `false` or not set | Compromised key could abuse the API without limits |
| `RotationDays` | Not set | No rotation policy configured |
| `RotationDays` | Greater than 365 | Rotation period is very long |
If API key authentication is explicitly disabled (`Enabled: false`), the check reports an informational result and exits.
## Why It Matters
API keys are the primary authentication mechanism for service-to-service communication and CI/CD integrations. Short keys can be brute-forced. Keys passed in query strings are recorded in web server access logs, proxy logs, and browser history, creating exposure vectors. Without per-key rate limiting, a compromised key allows unlimited API abuse. Without rotation, a leaked key remains valid indefinitely.
## Common Causes
- Minimum API key length configured below 32 characters
- API keys allowed in query strings (`AllowInQueryString: true`)
- Using the `Authorization` header for API keys, conflicting with JWT/OAuth
- Per-key rate limiting not enabled
- API key rotation policy not configured or set to more than 365 days
## How to Fix
### Docker Compose
Set API key security configuration in environment variables:
```yaml
environment:
ApiKey__MinLength: "32"
ApiKey__AllowInQueryString: "false"
ApiKey__HeaderName: "X-API-Key"
ApiKey__RateLimitPerKey: "true"
ApiKey__RotationDays: "90"
```
### Bare Metal / systemd
Edit `appsettings.json`:
```json
{
"ApiKey": {
"Enabled": true,
"MinLength": 32,
"HeaderName": "X-API-Key",
"AllowInQueryString": false,
"RateLimitPerKey": true,
"RotationDays": 90
}
}
```
### Kubernetes / Helm
Set in Helm values:
```yaml
apiKey:
minLength: 32
headerName: "X-API-Key"
allowInQueryString: false
rateLimitPerKey: true
rotationDays: 90
```
## Verification
```
stella doctor run --check check.security.apikey
```
## Related Checks
- `check.security.ratelimit` validates global rate limiting configuration
- `check.security.secrets` ensures API keys are not stored as plain text
- `check.core.auth.config` validates overall authentication configuration

View File

@@ -0,0 +1,93 @@
---
checkId: check.security.audit.logging
plugin: stellaops.doctor.security
severity: warn
tags: [security, audit, logging]
---
# Audit Logging
## What It Checks
Validates that audit logging is enabled and properly configured for security events. The check inspects configuration under `Audit:*`, `Security:Audit:*`, and `Logging:Audit:*` sections:
| Setting | Expected | Issue if not met |
|---|---|---|
| `Enabled` | `true` | Audit logging explicitly disabled or not configured |
| `LogAuthenticationEvents` | `true` | Authentication events not being logged |
| `LogAdministrativeEvents` | `true` | Admin actions not being logged |
| `Destination` | Non-empty | Audit log destination not configured |
The check also reads `LogAccessEvents` (data access logging) for reporting, but does not flag it as an issue since it defaults to `false` and is optional.
If audit logging is explicitly disabled (`Enabled: false`), the check warns and recommends enabling it. If `Enabled` is not set at all, it flags this as a potential gap.
## Why It Matters
Audit logging is a compliance requirement for security frameworks (SOC 2, ISO 27001, FedRAMP). Without audit logs:
- Authentication failures and brute-force attempts go undetected.
- Administrative actions (user creation, permission changes, policy modifications) are untraceable.
- Incident response has no forensic evidence.
- Release decisions and approval workflows cannot be reconstructed.
Stella Ops is a release control plane where every decision must be auditable. Missing audit logs undermine the core value proposition.
## Common Causes
- Audit logging disabled in configuration
- Audit logging configuration not found (never explicitly enabled)
- Authentication event logging turned off
- Administrative event logging turned off
- Audit log destination not configured (logs go nowhere)
## How to Fix
### Docker Compose
Add audit configuration to environment variables:
```yaml
environment:
Audit__Enabled: "true"
Audit__LogAuthenticationEvents: "true"
Audit__LogAdministrativeEvents: "true"
Audit__LogAccessEvents: "true"
Audit__Destination: "database"
```
### Bare Metal / systemd
Edit `appsettings.json`:
```json
{
"Audit": {
"Enabled": true,
"LogAuthenticationEvents": true,
"LogAccessEvents": true,
"LogAdministrativeEvents": true,
"Destination": "database"
}
}
```
Restart the service:
```bash
sudo systemctl restart stellaops-platform
```
### Kubernetes / Helm
Set in Helm values:
```yaml
audit:
enabled: true
logAuthenticationEvents: true
logAccessEvents: true
logAdministrativeEvents: true
destination: "database"
```
## Verification
```
stella doctor run --check check.security.audit.logging
```
## Related Checks
- `check.security.secrets` — ensures audit log credentials are not exposed
- `check.core.config.loaded` — audit logging depends on configuration being loaded

View File

@@ -0,0 +1,88 @@
---
checkId: check.security.cors
plugin: stellaops.doctor.security
severity: warn
tags: [security, cors, web]
---
# CORS Configuration
## What It Checks
Validates Cross-Origin Resource Sharing (CORS) security settings. The check inspects `Cors:*` and `Security:Cors:*` configuration sections:
| Condition | Severity | Issue |
|---|---|---|
| `AllowAnyOrigin` is `true` | `fail` | Any origin can make cross-origin requests |
| `AllowAnyOrigin` + `AllowCredentials` both true | `fail` | Critical: any origin can send credentialed requests |
| Wildcard `*` in `AllowedOrigins` array | `warn` | Wildcard provides no protection |
| No allowed origins configured | `warn` | CORS origins not explicitly defined |
| Non-HTTPS origin (except localhost/127.0.0.1) | `warn` | Non-HTTPS origins are insecure |
Evidence collected includes: allowed origins list (up to 5), `AllowCredentials` flag, `AllowAnyOrigin` flag, and configured methods.
## Why It Matters
Overly permissive CORS configuration allows malicious websites to make authenticated API requests on behalf of logged-in users. If `AllowAnyOrigin` is combined with `AllowCredentials`, an attacker's site can read responses from the Stella Ops API using the victim's session cookies. This can lead to data exfiltration, unauthorized release approvals, or policy modifications.
## Common Causes
- CORS allows any origin (`AllowAnyOrigin: true`) -- common in development, dangerous in production
- CORS wildcard origin `*` configured in the allowed origins list
- CORS allows any origin with credentials enabled simultaneously
- Allowed origins include non-HTTPS URLs in production
- No CORS allowed origins configured at all
## How to Fix
### Docker Compose
Set explicit CORS origins in environment variables:
```yaml
environment:
Cors__AllowAnyOrigin: "false"
Cors__AllowCredentials: "true"
Cors__AllowedOrigins__0: "https://stella-ops.local"
Cors__AllowedOrigins__1: "https://console.stella-ops.local"
Cors__AllowedMethods__0: "GET"
Cors__AllowedMethods__1: "POST"
Cors__AllowedMethods__2: "PUT"
Cors__AllowedMethods__3: "DELETE"
```
### Bare Metal / systemd
Edit `appsettings.json`:
```json
{
"Cors": {
"AllowAnyOrigin": false,
"AllowCredentials": true,
"AllowedOrigins": [
"https://stella-ops.yourdomain.com"
],
"AllowedMethods": ["GET", "POST", "PUT", "DELETE"]
}
}
```
### Kubernetes / Helm
Set in Helm values:
```yaml
cors:
allowAnyOrigin: false
allowCredentials: true
allowedOrigins:
- "https://stella-ops.yourdomain.com"
allowedMethods:
- GET
- POST
- PUT
- DELETE
```
## Verification
```
stella doctor run --check check.security.cors
```
## Related Checks
- `check.security.headers` — validates other HTTP security headers (HSTS, CSP, X-Frame-Options)
- `check.core.auth.config` — authentication must complement CORS to prevent unauthorized access

View File

@@ -0,0 +1,94 @@
---
checkId: check.security.encryption
plugin: stellaops.doctor.security
severity: warn
tags: [security, encryption, cryptography]
---
# Encryption Keys
## What It Checks
Validates encryption key configuration and algorithms. The check only runs when an encryption configuration section exists (`Encryption`, `DataProtection`, or `Cryptography`). It inspects:
| Setting | Threshold/Condition | Severity |
|---|---|---|
| `Algorithm` | Contains DES, 3DES, RC4, MD5, or SHA1 | `fail` — weak algorithm |
| `KeySize` | Less than 128 bits | `fail` — key too small |
| `KeyRotationDays` | Greater than 365 | `warn` — infrequent rotation |
| `DataProtection:KeysPath` | Directory does not exist | `warn` — keys path missing |
Defaults if not explicitly configured: algorithm is `AES-256`.
Evidence collected includes: configured algorithm, key size, key rotation period, and data protection keys path.
## Why It Matters
Encryption protects data at rest and data protection keys used by ASP.NET Core for cookie encryption, anti-forgery tokens, and TempData. Weak algorithms (DES, 3DES, RC4) have known vulnerabilities and can be broken with modern hardware. Small key sizes reduce the keyspace, making brute-force attacks feasible. Without key rotation, a compromised key provides indefinite access to all encrypted data.
## Common Causes
- Weak encryption algorithm configured (DES, 3DES, RC4, MD5, SHA1)
- Encryption key size too small (less than 128 bits)
- Key rotation period greater than 365 days or not configured
- Data protection keys directory does not exist on disk
## How to Fix
### Docker Compose
Set encryption configuration:
```yaml
environment:
Encryption__Algorithm: "AES-256"
Encryption__KeySize: "256"
Encryption__KeyRotationDays: "90"
DataProtection__KeysPath: "/app/keys"
volumes:
- stellaops-keys:/app/keys
```
### Bare Metal / systemd
Edit `appsettings.json`:
```json
{
"Encryption": {
"Algorithm": "AES-256",
"KeySize": 256,
"KeyRotationDays": 90
},
"DataProtection": {
"KeysPath": "/var/lib/stellaops/keys"
}
}
```
Create the keys directory:
```bash
sudo mkdir -p /var/lib/stellaops/keys
sudo chown stellaops:stellaops /var/lib/stellaops/keys
sudo chmod 700 /var/lib/stellaops/keys
```
### Kubernetes / Helm
Set in Helm values and use a PersistentVolume for key storage:
```yaml
encryption:
algorithm: "AES-256"
keySize: 256
keyRotationDays: 90
dataProtection:
persistentVolume:
enabled: true
size: "100Mi"
```
## Verification
```
stella doctor run --check check.security.encryption
```
## Related Checks
- `check.core.crypto.available` — verifies cryptographic algorithms are available at the OS level
- `check.security.secrets` — ensures encryption keys are not stored as plain text in configuration
- `check.security.tls.certificate` — validates TLS certificate for encryption in transit

View File

@@ -0,0 +1,111 @@
---
checkId: check.security.evidence.integrity
plugin: stellaops.doctor.security
severity: fail
tags: [security, evidence, integrity, dsse, rekor, offline]
---
# Evidence Integrity
## What It Checks
Validates DSSE signatures, Rekor inclusion proofs, and evidence hash consistency for files in the evidence locker. The check only runs when `EvidenceLocker:LocalPath` or `Evidence:BasePath` is configured and the directory exists.
The check scans up to **100 evidence files** (`.json` and `.dsse`) and performs structural verification on three evidence formats:
### DSSE Envelopes
- Payload must be valid base64.
- At least one signature must exist.
- Each signature must have `keyid` and `sig` fields, with `sig` being valid base64.
- If `payloadDigest` is present, verifies SHA-256 digest matches the payload bytes.
### Evidence Bundles
- Manifest must have a `version` field.
- If `rekorReceipt` is present, validates the Rekor receipt structure.
### Rekor Receipts
- Must have non-empty `uuid`.
- Must have numeric `logIndex`.
- Must have `inclusionProof` with a non-empty `hashes` array.
### Content Digest
- Must have algorithm prefix (`sha256:` or `sha512:`).
Files that don't match any known format are skipped. Files that fail to parse as JSON are marked invalid.
## Why It Matters
Evidence integrity is the foundation of Stella Ops' auditability guarantee. Every release decision, scan result, and policy evaluation is recorded as signed evidence. If evidence files are tampered with, the entire audit trail becomes untrustworthy. Broken DSSE signatures mean attestations may have been modified after signing. Missing or invalid Rekor inclusion proofs mean the transparency log cannot verify the evidence was recorded.
## Common Causes
- Evidence files may have been tampered with or corrupted
- DSSE signatures are invalid (payload was modified after signing)
- Evidence digests do not match content (partial writes, disk corruption)
- Rekor inclusion proofs are invalid or missing required fields
- Evidence locker directory does not exist or has not been initialized
## How to Fix
### Docker Compose
Verify the evidence locker path is configured and accessible:
```yaml
environment:
EvidenceLocker__LocalPath: "/data/evidence"
volumes:
- stellaops-evidence:/data/evidence
```
Investigate invalid files:
```bash
# List evidence files
docker compose exec platform ls -la /data/evidence/
# Check a specific file
docker compose exec platform cat /data/evidence/<file>.json | jq
```
Re-generate affected evidence:
```bash
# Re-scan and re-sign evidence bundles
docker compose exec platform stella evidence regenerate --path /data/evidence/<file>
```
### Bare Metal / systemd
```bash
# Create the evidence directory if missing
mkdir -p /var/lib/stellaops/evidence
chown stellaops:stellaops /var/lib/stellaops/evidence
# Verify file integrity
sha256sum /var/lib/stellaops/evidence/*.json
# Check Rekor entries
rekor-cli get --uuid <uuid-from-evidence>
```
### Kubernetes / Helm
Ensure evidence is stored on a persistent volume:
```yaml
evidenceLocker:
localPath: "/data/evidence"
persistentVolume:
enabled: true
size: "10Gi"
storageClass: "standard"
```
Verify inside the pod:
```bash
kubectl exec -it <pod> -- ls -la /data/evidence/
kubectl exec -it <pod> -- stella doctor run --check check.security.evidence.integrity
```
## Verification
```
stella doctor run --check check.security.evidence.integrity
```
## Related Checks
- `check.security.encryption` — validates encryption keys used for evidence signing
- `check.core.crypto.available` — SHA-256 must be available for digest verification
- `check.core.env.diskspace` — insufficient disk space can cause incomplete evidence writes

View File

@@ -0,0 +1,109 @@
---
checkId: check.security.headers
plugin: stellaops.doctor.security
severity: warn
tags: [security, headers, web]
---
# Security Headers
## What It Checks
Validates that HTTP security headers are properly configured. The check inspects `Security:Headers:*` and `Headers:*` configuration sections for five critical headers:
| Header | Setting | Issue if missing/wrong |
|---|---|---|
| **HSTS** | `Hsts:Enabled` | Not enabled — browsers won't enforce HTTPS |
| **X-Frame-Options** | `XFrameOptions` | Not configured — clickjacking vulnerability |
| **X-Frame-Options** | Set to `ALLOWALL` | Provides no protection |
| **Content-Security-Policy** | `ContentSecurityPolicy` / `Csp` | Not configured — XSS and injection risks |
| **X-Content-Type-Options** | `XContentTypeOptions` | Not enabled — MIME type sniffing vulnerability |
| **Referrer-Policy** | `ReferrerPolicy` | Not configured — referrer information leaks |
The check reports a warning listing all unconfigured headers.
## Why It Matters
Security headers are a defense-in-depth measure that protects against common web attacks:
- **HSTS**: Forces browsers to use HTTPS, preventing SSL-stripping attacks.
- **X-Frame-Options**: Prevents the UI from being embedded in iframes on malicious sites (clickjacking).
- **Content-Security-Policy**: Prevents cross-site scripting (XSS) and other code injection attacks.
- **X-Content-Type-Options**: Prevents browsers from interpreting files as a different MIME type.
- **Referrer-Policy**: Controls how much referrer information is included with requests, preventing data leaks.
## Common Causes
- HSTS not enabled (common in development environments)
- X-Frame-Options header not configured or set to ALLOWALL
- Content-Security-Policy header not defined
- X-Content-Type-Options: nosniff not enabled
- Referrer-Policy header not configured
- Security headers middleware not added to the ASP.NET Core pipeline
## How to Fix
### Docker Compose
Set security headers via environment variables:
```yaml
environment:
Security__Headers__Hsts__Enabled: "true"
Security__Headers__XFrameOptions: "DENY"
Security__Headers__ContentSecurityPolicy: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
Security__Headers__XContentTypeOptions__Enabled: "true"
Security__Headers__ReferrerPolicy: "strict-origin-when-cross-origin"
```
### Bare Metal / systemd
Edit `appsettings.json`:
```json
{
"Security": {
"Headers": {
"Hsts": {
"Enabled": true
},
"XFrameOptions": "DENY",
"ContentSecurityPolicy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
"XContentTypeOptions": {
"Enabled": true
},
"ReferrerPolicy": "strict-origin-when-cross-origin"
}
}
}
```
### Kubernetes / Helm
Set in Helm values:
```yaml
security:
headers:
hsts:
enabled: true
xFrameOptions: "DENY"
contentSecurityPolicy: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
xContentTypeOptions:
enabled: true
referrerPolicy: "strict-origin-when-cross-origin"
```
Alternatively, configure at the ingress level:
```yaml
ingress:
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
```
## Verification
```
stella doctor run --check check.security.headers
```
## Related Checks
- `check.security.cors` — CORS headers are another critical web security mechanism
- `check.security.tls.certificate` — HSTS requires a valid TLS certificate

View File

@@ -0,0 +1,104 @@
---
checkId: check.security.jwt.config
plugin: stellaops.doctor.security
severity: fail
tags: [security, jwt, authentication]
---
# JWT Configuration
## What It Checks
Validates JWT token signing and validation configuration. The check only runs when a JWT configuration section exists (`Jwt` or `Authentication:Jwt`). It inspects:
| Setting | Threshold/Condition | Severity |
|---|---|---|
| `SigningKey` | Not configured | `fail` |
| `SigningKey` | Shorter than 32 characters | `fail` |
| `Issuer` | Not configured | `fail` |
| `Audience` | Not configured | `fail` |
| `ExpirationMinutes` | Greater than 1440 (24 hours) | `warn` |
| `Algorithm` | `none` | `fail` — completely insecure |
| `Algorithm` | `HS256` | `warn` — acceptable but RS256/ES256 recommended |
Default values if not explicitly set: `ExpirationMinutes` = 60, `Algorithm` = HS256.
Evidence collected includes: whether a signing key is configured, key length, issuer, audience, expiration minutes, and algorithm.
## Why It Matters
JWT tokens are the primary authentication mechanism for API access. A missing or short signing key allows token forgery. The `none` algorithm disables signature verification entirely. Missing issuer or audience values disable critical validation claims, allowing tokens from other systems to be accepted. Long expiration times increase the window of opportunity if a token is compromised.
## Common Causes
- JWT signing key is not configured in the deployment
- JWT signing key is too short (fewer than 32 characters)
- JWT issuer or audience not configured
- JWT expiration time set too long (more than 24 hours)
- Using algorithm `none` which disables all signature verification
- Using HS256 symmetric algorithm when asymmetric (RS256/ES256) would be more secure
## How to Fix
### Docker Compose
Set JWT configuration as environment variables:
```yaml
environment:
Jwt__SigningKey: "<generate-a-strong-key-at-least-32-chars>"
Jwt__Issuer: "https://stella-ops.local"
Jwt__Audience: "stellaops-api"
Jwt__ExpirationMinutes: "60"
Jwt__Algorithm: "RS256"
```
Generate a strong signing key:
```bash
openssl rand -base64 48
```
### Bare Metal / systemd
Edit `appsettings.json`:
```json
{
"Jwt": {
"SigningKey": "<strong-key>",
"Issuer": "https://stella-ops.yourdomain.com",
"Audience": "stellaops-api",
"ExpirationMinutes": 60,
"Algorithm": "RS256"
}
}
```
For RS256, generate a key pair:
```bash
openssl genrsa -out jwt-private.pem 2048
openssl rsa -in jwt-private.pem -pubout -out jwt-public.pem
```
### Kubernetes / Helm
Store the signing key as a Kubernetes Secret:
```bash
kubectl create secret generic stellaops-jwt \
--from-literal=signing-key="$(openssl rand -base64 48)"
```
Reference in Helm values:
```yaml
jwt:
issuer: "https://stella-ops.yourdomain.com"
audience: "stellaops-api"
expirationMinutes: 60
algorithm: "RS256"
signingKeySecret: "stellaops-jwt"
```
## Verification
```
stella doctor run --check check.security.jwt.config
```
## Related Checks
- `check.core.auth.config` — validates broader authentication configuration including JWT
- `check.security.secrets` — ensures the JWT signing key is not stored as plain text
- `check.security.tls.certificate` — TLS protects JWT tokens in transit

View File

@@ -0,0 +1,95 @@
---
checkId: check.security.password.policy
plugin: stellaops.doctor.security
severity: warn
tags: [security, password, authentication]
---
# Password Policy
## What It Checks
Validates password requirements meet security standards. The check only runs when a password policy configuration section exists (`Identity:Password`, `Password`, or `Security:Password`). It inspects:
| Setting | Threshold | Severity |
|---|---|---|
| `RequiredLength` / `MinLength` | Less than 8 | `fail` (if < 6), otherwise `warn` |
| `RequiredLength` / `MinLength` | Less than 12 | `warn` 12+ recommended |
| `RequireDigit` | `false` | `warn` |
| `RequireLowercase` | `false` | `warn` |
| `RequireUppercase` | `false` | `warn` |
| `RequireNonAlphanumeric` / `RequireSpecialChar` | `false` | `warn` |
| `MaxFailedAccessAttempts` / `MaxAttempts` | Greater than 10 | `warn` |
| `DefaultLockoutTimeSpan` / `DurationMinutes` | Less than 1 minute | `warn` |
Default values if not explicitly set: min length = 8, require digit/lowercase/uppercase/special = true, max failed attempts = 5, lockout duration = 5 minutes.
## Why It Matters
Weak password policies enable brute-force and credential-stuffing attacks. Short passwords with low complexity can be cracked quickly with dictionary attacks. Without account lockout or with too many allowed attempts, automated attacks can run indefinitely. In a release control plane, compromised credentials could lead to unauthorized release approvals, policy changes, or data exfiltration.
## Common Causes
- Minimum password length set too short (below 8 characters)
- Password complexity requirements disabled (no digit, uppercase, lowercase, or special character requirement)
- Maximum failed login attempts too high (above 10), allowing extended brute-force
- Account lockout duration too short (less than 1 minute)
## How to Fix
### Docker Compose
Set password policy via environment variables:
```yaml
environment:
Identity__Password__RequiredLength: "12"
Identity__Password__RequireDigit: "true"
Identity__Password__RequireLowercase: "true"
Identity__Password__RequireUppercase: "true"
Identity__Password__RequireNonAlphanumeric: "true"
Identity__Lockout__MaxFailedAccessAttempts: "5"
Identity__Lockout__DefaultLockoutTimeSpan: "15"
```
### Bare Metal / systemd
Edit `appsettings.json`:
```json
{
"Identity": {
"Password": {
"RequiredLength": 12,
"RequireDigit": true,
"RequireLowercase": true,
"RequireUppercase": true,
"RequireNonAlphanumeric": true
},
"Lockout": {
"MaxFailedAccessAttempts": 5,
"DefaultLockoutTimeSpan": 15
}
}
}
```
### Kubernetes / Helm
Set in Helm values:
```yaml
identity:
password:
requiredLength: 12
requireDigit: true
requireLowercase: true
requireUppercase: true
requireNonAlphanumeric: true
lockout:
maxFailedAccessAttempts: 5
defaultLockoutTimeSpan: 15
```
## Verification
```
stella doctor run --check check.security.password.policy
```
## Related Checks
- `check.core.auth.config` validates overall authentication configuration
- `check.security.audit.logging` authentication failure events should be logged
- `check.security.ratelimit` rate limiting provides an additional layer of brute-force protection

View File

@@ -0,0 +1,99 @@
---
checkId: check.security.ratelimit
plugin: stellaops.doctor.security
severity: warn
tags: [security, ratelimit, api]
---
# Rate Limiting
## What It Checks
Validates that rate limiting is configured to prevent API abuse. The check inspects `RateLimiting:*` and `Security:RateLimiting:*` configuration sections:
| Condition | Result |
|---|---|
| `Enabled` not set at all | `info` — rate limiting configuration not found |
| `Enabled` is `false` | `warn` — rate limiting explicitly disabled |
| `PermitLimit` > 10,000 | `warn` — permit count very high |
| `WindowSeconds` < 1 | `warn` window too short |
| `WindowSeconds` > 3,600 | `warn` — window too long for burst prevention |
| Effective rate > 1,000 req/s | `warn` — rate may be too permissive |
The effective rate is calculated as `PermitLimit / WindowSeconds`.
Default values if not explicitly set: `PermitLimit` = 100, `WindowSeconds` = 60, `QueueLimit` = 0.
Evidence collected includes: enabled state, permit limit, window seconds, queue limit, and effective requests per second.
## Why It Matters
Without rate limiting, the API is vulnerable to denial-of-service attacks, credential-stuffing, and resource exhaustion. A single client or compromised API key can overwhelm the service, affecting all users. Rate limiting is especially important for:
- Login endpoints (prevents brute-force attacks)
- Scan submission endpoints (prevents resource exhaustion)
- Evidence upload endpoints (prevents storage exhaustion)
## Common Causes
- Rate limiting explicitly disabled in configuration
- Rate limiting configuration section not present
- Permit limit set too high (greater than 10,000 per window)
- Rate limit window too short (less than 1 second) or too long (greater than 1 hour)
- Effective rate too permissive (more than 1,000 requests per second)
## How to Fix
### Docker Compose
Set rate limiting configuration:
```yaml
environment:
RateLimiting__Enabled: "true"
RateLimiting__PermitLimit: "100"
RateLimiting__WindowSeconds: "60"
RateLimiting__QueueLimit: "10"
```
### Bare Metal / systemd
Edit `appsettings.json`:
```json
{
"RateLimiting": {
"Enabled": true,
"PermitLimit": 100,
"WindowSeconds": 60,
"QueueLimit": 10
}
}
```
### Kubernetes / Helm
Set in Helm values:
```yaml
rateLimiting:
enabled: true
permitLimit: 100
windowSeconds: 60
queueLimit: 10
```
For stricter per-endpoint limits, configure additional policies:
```yaml
rateLimiting:
policies:
login:
permitLimit: 10
windowSeconds: 300
scan:
permitLimit: 20
windowSeconds: 60
```
## Verification
```
stella doctor run --check check.security.ratelimit
```
## Related Checks
- `check.security.apikey` — per-key rate limiting for API key authentication
- `check.security.password.policy` — lockout policy provides complementary brute-force protection

View File

@@ -0,0 +1,138 @@
---
checkId: check.security.secrets
plugin: stellaops.doctor.security
severity: fail
tags: [security, secrets, configuration]
---
# Secrets Configuration
## What It Checks
Validates that secrets are properly managed and not exposed as plain text in configuration. The check scans the following configuration keys for potential plain-text secrets:
| Key | What it protects |
|---|---|
| `Jwt:SigningKey` | JWT token signing |
| `Jwt:Secret` | JWT secret (alternative key) |
| `ApiKey` | API authentication key |
| `ApiSecret` | API secret |
| `S3:SecretKey` | Object storage credentials |
| `Smtp:Password` | Email server credentials |
| `Ldap:Password` | Directory service credentials |
| `Redis:Password` | Cache/message broker credentials |
| `Valkey:Password` | Cache/message broker credentials |
A value is considered a plain-text secret if it:
1. Is at least 8 characters long.
2. Contains both uppercase and lowercase letters.
3. Contains digits or special characters.
4. Does NOT start with a secrets provider prefix: `vault:`, `azurekv:`, `aws:`, `gcp:`, `${`, or `@Microsoft.KeyVault`.
The check also examines whether a secrets management provider is configured (`Secrets:Provider`, `KeyVault:Provider`, `Secrets:VaultUrl`, `KeyVault:Url`, `Vault:Address`). A missing secrets manager is only flagged if plain-text secrets are also found.
Note: Connection strings are intentionally excluded from this check as they are DSNs (host/port/db) and are expected in configuration.
## Why It Matters
Plain-text secrets in configuration files are a critical security risk. Configuration files are often committed to version control, stored in CI artifacts, or readable by anyone with filesystem access. Leaked secrets enable:
- Token forgery (JWT signing keys).
- Unauthorized API access (API keys).
- Data access via backend services (database, SMTP, LDAP passwords).
- Lateral movement within the infrastructure.
## Common Causes
- Secrets stored directly in `appsettings.json` instead of using a secrets provider
- Environment variables containing secrets not sourced from a secrets manager
- Development secrets left in production configuration
- No secrets management provider configured (HashiCorp Vault, Azure Key Vault, etc.)
## How to Fix
### Docker Compose
Use Docker secrets or reference an external secrets manager:
```yaml
services:
platform:
environment:
Jwt__SigningKey: "vault:secret/data/stellaops/jwt#signing_key"
Secrets__Provider: "vault"
Secrets__VaultUrl: "http://vault:8200"
secrets:
- jwt_signing_key
secrets:
jwt_signing_key:
file: ./secrets/jwt_signing_key.txt
```
Or use `dotnet user-secrets` for development:
```bash
dotnet user-secrets set "Jwt:SigningKey" "<your-secret>"
```
### Bare Metal / systemd
Configure a secrets provider in `appsettings.json`:
```json
{
"Secrets": {
"Provider": "vault",
"VaultUrl": "https://vault.internal:8200",
"UseSecretManager": true
}
}
```
Store secrets in the provider instead of config files:
```bash
# HashiCorp Vault
vault kv put secret/stellaops/jwt signing_key="<key>"
# dotnet user-secrets (development)
dotnet user-secrets set "Jwt:SigningKey" "<key>"
```
### Kubernetes / Helm
Store secrets as Kubernetes Secrets:
```bash
kubectl create secret generic stellaops-secrets \
--from-literal=jwt-signing-key="<key>" \
--from-literal=smtp-password="<password>"
```
Reference in Helm values:
```yaml
secrets:
provider: "kubernetes"
existingSecret: "stellaops-secrets"
```
Or use an external secrets operator (e.g., External Secrets Operator with Vault):
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: stellaops-secrets
spec:
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: stellaops-secrets
data:
- secretKey: jwt-signing-key
remoteRef:
key: secret/stellaops/jwt
property: signing_key
```
## Verification
```
stella doctor run --check check.security.secrets
```
## Related Checks
- `check.security.jwt.config` — JWT signing key security
- `check.security.encryption` — encryption key management
- `check.security.apikey` — API key security practices

View File

@@ -0,0 +1,114 @@
---
checkId: check.security.tls.certificate
plugin: stellaops.doctor.security
severity: fail
tags: [security, tls, certificate]
---
# TLS Certificate
## What It Checks
Validates TLS certificate validity and expiration. The check only runs when a certificate path is configured (`Tls:CertificatePath` or `Kestrel:Certificates:Default:Path`). It loads the certificate file and performs the following validations:
| Condition | Result |
|---|---|
| Certificate file not found | `fail` |
| Certificate cannot be loaded (corrupt, wrong password) | `fail` |
| Certificate not yet valid (`NotBefore` in the future) | `fail` |
| Certificate has expired (`NotAfter` in the past) | `fail` |
| Certificate expires in less than **30 days** | `warn` |
| Certificate valid for 30+ days | `pass` |
The check supports both PEM certificates and PKCS#12 (.pfx/.p12) files with optional passwords (`Tls:CertificatePassword` or `Kestrel:Certificates:Default:Password`).
Evidence collected includes: subject, issuer, NotBefore, NotAfter, days until expiry, and thumbprint.
## Why It Matters
An expired or invalid TLS certificate causes all HTTPS connections to fail. Browsers display security warnings, API clients reject responses, and inter-service communication breaks. In a release control plane, TLS failures prevent:
- Console access for operators.
- API calls from CI/CD pipelines.
- Inter-service communication via HTTPS.
- OIDC authentication flows with the Authority.
Certificate expiration is the most common cause of production outages that is entirely preventable with monitoring.
## Common Causes
- Certificate file path is incorrect or the file was deleted
- Certificate has exceeded its validity period (expired)
- Certificate validity period has not started yet (clock skew or pre-dated certificate)
- Certificate file is corrupted
- Certificate password is incorrect (for PKCS#12 files)
- Certificate format not supported
## How to Fix
### Docker Compose
Mount the certificate and configure the path:
```yaml
services:
platform:
environment:
Tls__CertificatePath: "/app/certs/stellaops.pfx"
Tls__CertificatePassword: "${TLS_CERT_PASSWORD}"
volumes:
- ./certs/stellaops.pfx:/app/certs/stellaops.pfx:ro
```
Generate a new self-signed certificate for development:
```bash
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes \
-subj "/CN=stella-ops.local"
openssl pkcs12 -export -out stellaops.pfx -inkey key.pem -in cert.pem
```
### Bare Metal / systemd
Renew the certificate (e.g., with Let's Encrypt):
```bash
sudo certbot renew
sudo systemctl restart stellaops-platform
```
Or update the configuration with a new certificate:
```bash
# Update appsettings.json
{
"Tls": {
"CertificatePath": "/etc/ssl/stellaops/cert.pfx",
"CertificatePassword": "<password>"
}
}
```
### Kubernetes / Helm
Use cert-manager for automatic certificate management:
```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: stellaops-tls
spec:
secretName: stellaops-tls-secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- stella-ops.yourdomain.com
```
Reference in Helm values:
```yaml
tls:
secretName: "stellaops-tls-secret"
```
## Verification
```
stella doctor run --check check.security.tls.certificate
```
## Related Checks
- `check.security.headers` — HSTS requires a valid TLS certificate
- `check.security.encryption` — validates encryption at rest (TLS handles encryption in transit)
- `check.core.crypto.available` — RSA/ECDSA must be available for certificate operations