Estimated time: 10-15 minutes Prerequisites: Docker, curl, jq (optional)
# Clone the project
git clone <repo-url> skylink
cd skylink
# Copy environment template
cp .env.example .env
# Generate RSA keys (if not already done)
openssl genrsa -out /tmp/private.pem 2048
openssl rsa -in /tmp/private.pem -pubout -out /tmp/public.pem
# Add keys to .env
echo "PRIVATE_KEY_PEM=\"$(cat /tmp/private.pem)\"" >> .env
echo "PUBLIC_KEY_PEM=\"$(cat /tmp/public.pem)\"" >> .env# Build and start (first time)
make build && make up
# Or simply
docker compose up -d
# Verify everything is UP
make statusExpected output:
NAME STATUS PORTS
gateway Up 0.0.0.0:8000->8000/tcp
telemetry Up 8001/tcp
weather Up 8002/tcp
contacts Up 8003/tcp
db Up 5432/tcp
make healthExpected output:
Gateway: healthy
Telemetry: healthy
Weather: healthy
Contacts: healthy
PostgreSQL: UP
# Generate a UUID for the aircraft
AIRCRAFT_ID=$(uuidgen || echo "550e8400-e29b-41d4-a716-446655440000")
# Get a JWT token
curl -s -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d "{\"aircraft_id\": \"$AIRCRAFT_ID\"}" | jqExpected output:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900
}# Extract and save the token
TOKEN=$(curl -s -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d "{\"aircraft_id\": \"$AIRCRAFT_ID\"}" | jq -r '.access_token')
echo "Token obtained: ${TOKEN:0:50}..."# Decode the payload (base64)
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | jqExpected output:
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"aud": "skylink",
"iat": 1734600000,
"exp": 1734600900
}Note: SkyLink implements RBAC with 5 roles and 7 permissions. Each role has least-privilege access.
# Get a token with aircraft_premium role (can access contacts)
TOKEN_PREMIUM=$(curl -s -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d '{
"aircraft_id": "550e8400-e29b-41d4-a716-446655440000",
"role": "aircraft_premium"
}' | jq -r '.access_token')
echo "Premium token: ${TOKEN_PREMIUM:0:50}..."echo $TOKEN_PREMIUM | cut -d'.' -f2 | base64 -d 2>/dev/null | jqExpected output:
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"aud": "skylink",
"iat": 1734600000,
"exp": 1734600900,
"role": "aircraft_premium"
}curl -s "http://localhost:8000/contacts/?person_fields=names" \
-H "Authorization: Bearer $TOKEN_PREMIUM" | jq '.items | length'Expected output:
5
# Get a token with default role (aircraft_standard - no contacts access)
TOKEN_STANDARD=$(curl -s -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d '{"aircraft_id": "550e8400-e29b-41d4-a716-446655440001"}' | jq -r '.access_token')
# Try to access contacts - should fail
curl -s "http://localhost:8000/contacts/?person_fields=names" \
-H "Authorization: Bearer $TOKEN_STANDARD" -w "\nHTTP Status: %{http_code}\n"Expected output:
{
"detail": "Permission denied: contacts:read required"
}
HTTP Status: 403# Ground control (read-only access)
TOKEN_GC=$(curl -s -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d '{"aircraft_id": "550e8400-e29b-41d4-a716-446655440002", "role": "ground_control"}' \
| jq -r '.access_token')
# Ground control CAN read contacts
curl -s "http://localhost:8000/contacts/?person_fields=names" \
-H "Authorization: Bearer $TOKEN_GC" -o /dev/null -w "Contacts: HTTP %{http_code}\n"
# Ground control CANNOT write telemetry
curl -s -X POST http://localhost:8000/telemetry/ingest \
-H "Authorization: Bearer $TOKEN_GC" \
-H "Content-Type: application/json" \
-d '{"aircraft_id": "550e8400-e29b-41d4-a716-446655440002", "event_id": "test-001", "ts": "2025-01-01T00:00:00Z", "metrics": {"speed": 50}}' \
-o /dev/null -w "Telemetry: HTTP %{http_code}\n"Expected output:
Contacts: HTTP 200
Telemetry: HTTP 403
| Role | weather:read | contacts:read | telemetry:write |
|---|---|---|---|
| aircraft_standard | ✅ | ❌ | ✅ |
| aircraft_premium | ✅ | ✅ | ✅ |
| ground_control | ✅ | ✅ | ❌ |
| maintenance | ✅ | ❌ | ✅ |
| admin | ✅ | ✅ | ✅ |
See AUTHORIZATION.md for complete documentation.
EVENT_ID=$(uuidgen)
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
curl -s -X POST http://localhost:8000/telemetry/ingest \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"aircraft_id\": \"$AIRCRAFT_ID\",
\"event_id\": \"$EVENT_ID\",
\"ts\": \"$TS\",
\"metrics\": {
\"speed\": 45.5,
\"gps\": {\"lat\": 48.8566, \"lon\": 2.3522}
}
}" -w "\nHTTP Status: %{http_code}\n"Expected output:
{
"status": "created",
"event_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
HTTP Status: 201# Same request = same result (idempotency)
curl -s -X POST http://localhost:8000/telemetry/ingest \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"aircraft_id\": \"$AIRCRAFT_ID\",
\"event_id\": \"$EVENT_ID\",
\"ts\": \"$TS\",
\"metrics\": {
\"speed\": 45.5,
\"gps\": {\"lat\": 48.8566, \"lon\": 2.3522}
}
}" -w "\nHTTP Status: %{http_code}\n"Expected output:
{
"status": "duplicate",
"event_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
HTTP Status: 200# Same event_id but different data = conflict
curl -s -X POST http://localhost:8000/telemetry/ingest \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"aircraft_id\": \"$AIRCRAFT_ID\",
\"event_id\": \"$EVENT_ID\",
\"ts\": \"$TS\",
\"metrics\": {
\"speed\": 120.0,
\"gps\": {\"lat\": 48.8566, \"lon\": 2.3522}
}
}" -w "\nHTTP Status: %{http_code}\n"Expected output:
{
"detail": {
"code": "TELEMETRY_CONFLICT",
"message": "Event with same event_id but different payload already exists."
}
}
HTTP Status: 409Note: Rate limiting is configured on
/weather/current(60 req/min per aircraft_id).
# Send 70 requests quickly (limit = 60/min)
for i in $(seq 1 70); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
"http://localhost:8000/weather/current?lat=48.8566&lon=2.3522" \
-H "Authorization: Bearer $TOKEN")
if [ "$STATUS" = "429" ]; then
echo "Rate limit reached at request $i (HTTP 429)"
break
fi
# Show progress
if [ $i -le 5 ] || [ $i -ge 58 ]; then
echo "Request $i: HTTP $STATUS"
elif [ $i -eq 6 ]; then
echo "..."
fi
doneExpected output:
Request 1: HTTP 200
Request 2: HTTP 200
...
Request 58: HTTP 200
Request 59: HTTP 200
Request 60: HTTP 200
Rate limit reached at request 61 (HTTP 429)
# The next request should be rate limited
curl -s "http://localhost:8000/weather/current?lat=48.8566&lon=2.3522" \
-H "Authorization: Bearer $TOKEN" -w "\nHTTP Status: %{http_code}\n"Expected output:
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded: 60 per 1 minute"
}
}
HTTP Status: 429curl -s http://localhost:8000/metrics | grep -E "^(http_|rate_)" | head -20Expected output:
# HELP http_requests_total Total number of requests by method, status and handler.
# TYPE http_requests_total counter
http_requests_total{handler="/health",method="GET",status="200"} 5.0
http_requests_total{handler="/auth/token",method="POST",status="200"} 2.0
http_requests_total{handler="/weather/current",method="GET",status="200"} 60.0
# HELP rate_limit_exceeded_total Total number of rate limit exceeded responses (429)
# TYPE rate_limit_exceeded_total counter
rate_limit_exceeded_total 10.0
# Request counter by status
curl -s http://localhost:8000/metrics | grep "http_requests_total"
# Rate-limit counter
curl -s http://localhost:8000/metrics | grep "rate_limit_exceeded"
# Latencies
curl -s http://localhost:8000/metrics | grep "http_request_duration_seconds"# Use -D - to display headers (GET request)
curl -s -D - http://localhost:8000/health -o /dev/nullExpected output:
HTTP/1.1 200 OK
content-type: application/json
x-trace-id: f6b40f74-bdd5-4865-9568-9cd2567eecf9
x-content-type-options: nosniff
x-frame-options: DENY
cache-control: no-store, no-cache, must-revalidate, max-age=0
pragma: no-cache
cross-origin-opener-policy: same-origin
cross-origin-embedder-policy: require-corp
referrer-policy: no-referrer
permissions-policy: geolocation=(), microphone=(), camera=()
# Send a custom trace_id
curl -s -D - http://localhost:8000/health \
-H "X-Trace-Id: my-custom-trace-123" -o /dev/null | grep -i traceExpected output:
x-trace-id: my-custom-trace-123
curl -s -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d '{
"aircraft_id": "550e8400-e29b-41d4-a716-446655440000",
"unknown_field": "malicious"
}' | jqExpected output:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": {
"fields": [
{
"field": "unknown_field",
"issue": "extra_forbidden",
"message": "Extra inputs are not permitted"
}
]
}
}
}curl -s -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d '{"aircraft_id": "not-a-valid-uuid"}' | jqExpected output:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": {
"fields": [
{
"field": "aircraft_id",
"issue": "uuid_parsing",
"message": "Input should be a valid UUID"
}
]
}
}
}# Get weather (requires lat/lon query params)
curl -s "http://localhost:8000/weather/current?lat=48.8566&lon=2.3522" \
-H "Authorization: Bearer $TOKEN" | jq '.location.name, .current.temp_c, .current.condition.text'Expected output (demo mode with Paris fixtures):
"Paris"
15
"Partly cloudy"Note: The full response includes
location(city details) andcurrent(temperature, conditions, wind, humidity, air quality).
# List contacts (Google People API format)
curl -s "http://localhost:8000/contacts/?person_fields=names,emailAddresses" \
-H "Authorization: Bearer $TOKEN" | jq '.items | length, .items[0].names[0].displayName'Expected output (demo mode with fixtures):
5
"Alice Dupont"Note: Contacts use the Google People API format. In demo mode, 5 fictional contacts are available.
Prerequisites: This demo works after the CI pipeline has signed the image. Locally, you can simulate verification with a signed image.
# macOS
brew install cosign
# Linux (via Go)
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# Or via Docker
alias cosign='docker run --rm gcr.io/projectsigstore/cosign:latest'# Replace with your GitLab registry
REGISTRY="registry.gitlab.com/your-group/skylink"
IMAGE_TAG="latest"
# Verify with the public key
cosign verify --key cosign.pub "$REGISTRY:$IMAGE_TAG"Expected output:
Verification for registry.gitlab.com/your-group/skylink:latest --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
[{"critical":{"identity":{"docker-reference":"registry.gitlab.com/..."},...}]
# Verify that the CycloneDX SBOM is attached
cosign verify-attestation \
--key cosign.pub \
--type cyclonedx \
"$REGISTRY:$IMAGE_TAG"Expected output:
Verification for registry.gitlab.com/your-group/skylink:latest --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
{"payloadType":"application/vnd.in-toto+json","payload":"..."}
# Download and decode the SBOM attestation
cosign verify-attestation \
--key cosign.pub \
--type cyclonedx \
"$REGISTRY:$IMAGE_TAG" 2>/dev/null \
| jq -r '.payload' \
| base64 -d \
| jq '.predicate'Expected output (excerpt):
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"version": 1,
"components": [
{"name": "fastapi", "version": "0.109.0", "type": "library"},
{"name": "pydantic", "version": "2.5.0", "type": "library"},
...
]
}# In the GitLab pipeline, the public key is a CI variable
# Verification is automatic after attest_sbom
# To simulate locally with the CI variable:
echo "$COSIGN_PUBLIC_KEY" > /tmp/cosign.pub
cosign verify --key /tmp/cosign.pub "$REGISTRY:$IMAGE_TAG"
rm /tmp/cosign.pubNote: The monitoring stack is optional and uses Docker Compose profiles.
# Start all services including monitoring
docker compose --profile monitoring up -d
# Verify monitoring services are running
docker compose --profile monitoring psExpected output:
NAME STATUS PORTS
gateway Up 0.0.0.0:8000->8000/tcp
telemetry Up 8001/tcp
weather Up 8002/tcp
contacts Up 8003/tcp
db Up 5432/tcp
prometheus Up 0.0.0.0:9090->9090/tcp
grafana Up 0.0.0.0:3000->3000/tcp
Open http://localhost:9090 in your browser.
# Verify Prometheus is scraping targets
curl -s http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health: .health}'Expected output:
{"job": "skylink-gateway", "health": "up"}
{"job": "skylink-telemetry", "health": "up"}
{"job": "skylink-weather", "health": "up"}
{"job": "skylink-contacts", "health": "up"}
{"job": "prometheus", "health": "up"}# Query total HTTP requests
curl -s 'http://localhost:9090/api/v1/query?query=http_requests_total' | jq '.data.result | length'
# Query authentication failures (401 responses)
curl -s 'http://localhost:9090/api/v1/query?query=http_requests_total{status="401"}' | jq '.data.result'
# Query request latency (p95)
curl -s 'http://localhost:9090/api/v1/query?query=histogram_quantile(0.95,sum(rate(http_request_duration_seconds_bucket[5m]))by(le))' | jq '.data.result[0].value[1]'Open http://localhost:3000 in your browser.
Credentials: admin / admin
Navigate to: Dashboards → SkyLink → SkyLink Security Dashboard
The dashboard includes:
- Authentication Success Rate (gauge) - shows 100% when no auth failures
- Client Errors by Status (pie chart) - shows "No data" if no 4xx errors (this is good!)
- Authentication Failures (time series) - shows "No data" if no 401/403 (this is good!)
- mTLS Failures (stat) - shows 0 if no mTLS errors
- Rate Limited Requests (time series) - shows "No data" if no 429 (this is good!)
- Request Latency p50/p95/p99 (time series) - requires traffic to display
- Service Status (up/down indicators)
Note: Security panels showing "No data" is expected behavior when there are no security incidents. See MONITORING.md for details.
# List all alert rules
curl -s http://localhost:9090/api/v1/rules | jq '.data.groups[].rules[] | {name: .name, state: .state}'
# Check for firing alerts
curl -s http://localhost:9090/api/v1/alerts | jq '.data.alerts[] | select(.state=="firing")'# Generate some traffic to see metrics
for i in $(seq 1 20); do
curl -s http://localhost:8000/health > /dev/null
curl -s -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d '{"aircraft_id": "550e8400-e29b-41d4-a716-446655440000"}' > /dev/null
done
echo "Traffic generated. Refresh Grafana dashboard to see metrics."Note: These scripts generate new cryptographic keys. They do NOT modify running services.
# Preview what the script will do
./scripts/rotate_jwt_keys.sh --dry-runExpected output:
╔══════════════════════════════════════════════════════════════╗
║ SkyLink JWT Key Rotation Script ║
╚══════════════════════════════════════════════════════════════╝
[INFO] Configuration:
Output Directory: /path/to/keys_new
Key Size: 2048 bits
...
Dry Run: true
[WARNING] DRY RUN MODE - No keys will be generated
Would perform the following actions:
1. Create directory: ./keys_new
2. Generate RSA private key (2048 bits)
3. Extract public key from private key
4. Create key ID file
# Generate new JWT keys
./scripts/rotate_jwt_keys.sh --output /tmp/jwt_demo_keys
# View the generated files
ls -la /tmp/jwt_demo_keys/Expected output:
private.pem (RSA private key - KEEP SECURE)
public.pem (RSA public key)
kid.txt (Key ID for JWKS)
# Generate new AES-256 encryption key
./scripts/rotate_encryption_key.sh --output /tmp/enc_demo_keys --env-formatExpected output:
╔══════════════════════════════════════════════════════════════╗
║ SkyLink AES-256 Encryption Key Rotation Script ║
╚══════════════════════════════════════════════════════════════╝
[SUCCESS] Encryption key generated
[SUCCESS] Key length validation: OK (64 characters)
# AES-256 Encryption Key
ENCRYPTION_KEY="a1b2c3d4e5f6..."
# First, generate a CA if not exists
./scripts/generate_ca.sh
# Preview server certificate renewal
./scripts/renew_certificates.sh --dry-run server
# Preview client certificate renewal
./scripts/renew_certificates.sh --dry-run client aircraft-001# Remove demo keys (NEVER commit these to git)
rm -rf /tmp/jwt_demo_keys /tmp/enc_demo_keysNote: Audit logs are automatically generated for all security-relevant events.
# Follow gateway logs and filter for AUDIT events
docker compose logs -f gateway 2>&1 | grep "AUDIT:"In another terminal, trigger some events:
# 1. Authentication success event
TOKEN=$(curl -s -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d '{"aircraft_id": "550e8400-e29b-41d4-a716-446655440000"}' | jq -r '.access_token')
# 2. Weather access event (rounds coordinates for privacy)
curl -s "http://localhost:8000/weather/current?lat=48.8566&lon=2.3522" \
-H "Authorization: Bearer $TOKEN" > /dev/null
# 3. Contacts access event
curl -s "http://localhost:8000/contacts/?person_fields=names" \
-H "Authorization: Bearer $TOKEN" > /dev/null
# 4. Rate limit exceeded event (run after Demo 3)
for i in $(seq 1 70); do
curl -s "http://localhost:8000/weather/current?lat=48.8566&lon=2.3522" \
-H "Authorization: Bearer $TOKEN" > /dev/null
done# Get a single audit event and format it
docker compose logs gateway 2>&1 | grep "AUDIT:" | head -1 | \
sed 's/.*AUDIT: //' | jqExpected output:
{
"timestamp": "2025-01-15T10:30:00.000000Z",
"event_id": "evt_abc123def456",
"event_type": "AUTH_SUCCESS",
"event_category": "authentication",
"severity": "info",
"service": "gateway",
"actor": {
"type": "aircraft",
"id": "550e8400-e29b-41d4-a716-446655440000",
"ip": "172.18.0.1"
},
"action": "authenticate",
"outcome": "success",
"trace_id": "f6b40f74-bdd5-4865-9568-9cd2567eecf9",
"details": {
"method": "jwt_rs256"
}
}# Authentication events only
docker compose logs gateway 2>&1 | grep "AUDIT:" | grep "AUTH_"
# Rate limit events only
docker compose logs gateway 2>&1 | grep "AUDIT:" | grep "RATE_LIMIT"
# Data access events only
docker compose logs gateway 2>&1 | grep "AUDIT:" | grep -E "(WEATHER|CONTACTS|TELEMETRY)"# Search for potential PII patterns (should return nothing)
docker compose logs gateway 2>&1 | grep "AUDIT:" | grep -E "@|Bearer|eyJ|PRIVATE"
# If the above returns nothing, your logs are clean!
echo "✓ No PII detected in audit logs"| Event Type | Category | Severity | Trigger |
|---|---|---|---|
AUTH_SUCCESS |
authentication | info | Successful JWT token issuance |
AUTH_FAILURE |
authentication | warning | Failed authentication attempt |
RATE_LIMIT_EXCEEDED |
security | warning | Rate limit hit (429) |
WEATHER_ACCESSED |
data | info | Weather endpoint access |
CONTACTS_ACCESSED |
data | info | Contacts endpoint access |
TELEMETRY_CREATED |
data | info | New telemetry event ingested |
TELEMETRY_DUPLICATE |
data | info | Duplicate event (idempotent) |
TELEMETRY_CONFLICT |
data | warning | Conflict detected (409) |
MTLS_CN_MISMATCH |
security | error | Certificate CN ≠ JWT sub |
See AUDIT_LOGGING.md for complete documentation.
# Stop all services (including monitoring if started)
docker compose --profile monitoring down
# Or just stop core services
make down
# Remove everything (containers, volumes, images)
make cleanStages: lint -> test -> build -> scan -> sbom -> security-scan -> sign
Jobs:
- lint:ruff : OK (0 errors)
- lint:black : OK (formatted)
- lint:bandit : OK (0 HIGH)
- test:pytest : OK (323 tests, 82% coverage)
- build:docker : OK (4 images)
- scan:trivy : OK (0 CRITICAL)
- scan:gitleaks : OK (0 secrets)
- scan:pip-audit : OK (0 vulns)
- sbom:cyclonedx : OK (artifact generated)
- dast:zap : OK (baseline)
- sign:sign_image : OK (image signed with cosign)
- sign:attest_sbom : OK (SBOM attached)
- sign:verify_signature: OK (signature verified)
| Artifact | Description |
|---|---|
sbom.json |
Software Bill of Materials (CycloneDX) |
trivy-report.json |
Container vulnerability scan |
zap-report.html |
DAST ZAP report |
coverage.xml |
pytest coverage report |
| Code | Demo | Meaning |
|---|---|---|
| 200 | Token, Duplicate | Success |
| 201 | Telemetry | Resource created |
| 400 | Validation | Invalid field |
| 401 | Expired token | Unauthenticated |
| 409 | Conflict | Idempotency violated |
| 429 | Rate limit | Too many requests |
- Stack started (
make upordocker compose up -d) - Health check OK (
make health) - JWT token obtained
- Telemetry 201 Created
- Idempotency 200 OK (duplicate)
- Conflict 409 (different data)
- Rate limit 429
- Metrics /metrics accessible
- Security headers present
- Strict validation (extra fields rejected)
- Monitoring stack started (
docker compose --profile monitoring up -d) - Prometheus targets healthy (http://localhost:9090/targets)
- Grafana dashboard accessible (http://localhost:3000)
- Alert rules loaded
- JWT rotation dry-run works (
./scripts/rotate_jwt_keys.sh --dry-run) - Encryption key rotation works (
./scripts/rotate_encryption_key.sh --dry-run) - Certificate renewal works (
./scripts/renew_certificates.sh --dry-run server)
- Audit logs visible in gateway logs (
docker compose logs gateway | grep AUDIT) - AUTH_SUCCESS events logged on token creation
- WEATHER_ACCESSED events logged with rounded coordinates
- RATE_LIMIT_EXCEEDED events logged on 429
- No PII in audit logs (no emails, tokens, keys)
- Image signature verified (cosign verify)
- SBOM attestation verified (cosign verify-attestation)
- Helm chart installed (
helm install skylink ./kubernetes/skylink) - All pods running (
kubectl get pods -n skylink) - NetworkPolicies applied (
kubectl get networkpolicies -n skylink) - Helm tests pass (
helm test skylink -n skylink)
For a complete understanding of the security posture:
| Document | Description |
|---|---|
| THREAT_MODEL.md | STRIDE-based threat analysis covering 30+ threats |
| SECURITY_ARCHITECTURE.md | Data flow diagrams with trust boundaries |
| MONITORING.md | Security monitoring with Prometheus and Grafana |
| KEY_MANAGEMENT.md | Key rotation procedures and cryptographic inventory |
| AUDIT_LOGGING.md | Audit event logging, security event tracking |
| AUTHORIZATION.md | Role-Based Access Control (RBAC), permissions |
| KUBERNETES.md | Kubernetes deployment with Helm chart |
| TECHNICAL_DOCUMENTATION.md | Complete technical documentation with RRA compliance |