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:
83
docs/doctor/articles/security/apikey.md
Normal file
83
docs/doctor/articles/security/apikey.md
Normal 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
|
||||
93
docs/doctor/articles/security/audit-logging.md
Normal file
93
docs/doctor/articles/security/audit-logging.md
Normal 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
|
||||
88
docs/doctor/articles/security/cors.md
Normal file
88
docs/doctor/articles/security/cors.md
Normal 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
|
||||
94
docs/doctor/articles/security/encryption.md
Normal file
94
docs/doctor/articles/security/encryption.md
Normal 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
|
||||
111
docs/doctor/articles/security/evidence-integrity.md
Normal file
111
docs/doctor/articles/security/evidence-integrity.md
Normal 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
|
||||
109
docs/doctor/articles/security/headers.md
Normal file
109
docs/doctor/articles/security/headers.md
Normal 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
|
||||
104
docs/doctor/articles/security/jwt-config.md
Normal file
104
docs/doctor/articles/security/jwt-config.md
Normal 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
|
||||
95
docs/doctor/articles/security/password-policy.md
Normal file
95
docs/doctor/articles/security/password-policy.md
Normal 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
|
||||
99
docs/doctor/articles/security/ratelimit.md
Normal file
99
docs/doctor/articles/security/ratelimit.md
Normal 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
|
||||
138
docs/doctor/articles/security/secrets.md
Normal file
138
docs/doctor/articles/security/secrets.md
Normal 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
|
||||
114
docs/doctor/articles/security/tls-certificate.md
Normal file
114
docs/doctor/articles/security/tls-certificate.md
Normal 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
|
||||
Reference in New Issue
Block a user