C3: Security Components - Authentication, Encryption, and Secrets Management
Level: Component (C4 Model Level 3) Scope: Security Architecture (Identity Platform, Cloud KMS, Secret Manager, IAM) Primary Audience: Security Engineers, Compliance Officers, Platform Architects Last Updated: November 23, 2025
Overview
This diagram shows the detailed security architecture for CODITECT cloud infrastructure, including authentication, authorization, encryption, secrets management, and compliance controls.
Key Security Layers:
- Authentication: Identity Platform (Firebase Auth) with OAuth2
- Authorization: IAM, Workload Identity, RBAC
- Encryption: Cloud KMS for key management, TLS everywhere
- Secrets: Secret Manager for credential storage
- Compliance: Audit logging, data residency, GDPR
Security Component Diagram
Component Details
1. Identity Platform (Authentication)
Technology: Google Identity Platform (Firebase Auth) Purpose: User authentication, OAuth2 integration, JWT token management
Configuration:
Identity Platform Settings:
Providers:
- Google OAuth2 (enabled)
- GitHub OAuth2 (enabled)
- Email/Password (disabled for now)
- Microsoft Azure AD (future)
Token Settings:
Algorithm: RS256 (RSA signature)
Access Token Expiration: 1 hour
Refresh Token Expiration: 7 days
Issuer: https://securetoken.google.com/coditect-citus-prod
Audience: coditect-citus-prod
Security:
MFA: Optional (future)
Password Policy: N/A (OAuth only)
Session Management: Custom (JWT + Redis TTL)
Email Verification: Required for email/password (future)
OAuth2 Flow:
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from django.shortcuts import redirect
from django.conf import settings
from firebase_admin import auth
from apps.auth.services import IdentityService, JWTService
from apps.users.services import UserService
# User initiates login
@api_view(['GET'])
@permission_classes([AllowAny])
def login_google(request):
"""Initiate Google OAuth2 login flow."""
oauth_url = (
f"https://accounts.google.com/o/oauth2/v2/auth?"
f"client_id={settings.GOOGLE_CLIENT_ID}&"
f"redirect_uri={settings.REDIRECT_URI}&"
f"response_type=code&"
f"scope=openid email profile"
)
return redirect(oauth_url)
# OAuth callback
@api_view(['GET'])
@permission_classes([AllowAny])
def auth_callback(request):
"""Handle OAuth2 callback and create session."""
code = request.GET.get('code')
# Exchange code for tokens
identity_service = IdentityService()
token_response = identity_service.exchange_code_for_token(code)
# Verify ID token with Firebase
decoded_token = auth.verify_id_token(token_response["id_token"])
# Create or update user in database
user_service = UserService()
user = user_service.create_or_update_user(decoded_token)
# Return JWT for API access
jwt_service = JWTService()
jwt_token = jwt_service.create_jwt(user)
return Response({
"access_token": jwt_token,
"token_type": "Bearer"
})
JWT Token Structure:
{
"header": {
"alg": "RS256",
"typ": "JWT",
"kid": "abc123"
},
"payload": {
"iss": "https://securetoken.google.com/coditect-citus-prod",
"aud": "coditect-citus-prod",
"sub": "user_123",
"email": "user@example.com",
"email_verified": true,
"iat": 1700000000,
"exp": 1700003600,
"tenant_id": "tenant_456",
"roles": ["user"],
"hardware_fingerprint": "abc123def456"
},
"signature": "..."
}
JWT Validation Middleware:
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from django.db import connection
from firebase_admin import auth
class FirebaseAuthentication(BaseAuthentication):
"""Custom authentication backend for Firebase JWT tokens."""
def authenticate(self, request):
"""Authenticate request using Firebase JWT token."""
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if not auth_header.startswith('Bearer '):
return None # Let other authentication methods handle it
token = auth_header.split(' ')[1]
try:
# Verify JWT signature and expiration
decoded = auth.verify_id_token(token)
# Extract user context
user_id = decoded["sub"]
tenant_id = decoded.get("tenant_id")
# Set PostgreSQL RLS context
with connection.cursor() as cursor:
cursor.execute(
"SET app.current_tenant = %s",
[str(tenant_id)]
)
# Create user object with decoded claims
user = type('User', (), {
'id': user_id,
'tenant_id': tenant_id,
'is_authenticated': True,
'claims': decoded
})()
return (user, token)
except auth.InvalidIdTokenError:
raise AuthenticationFailed('Invalid authentication token')
except auth.ExpiredIdTokenError:
raise AuthenticationFailed('Token expired')
# Usage in views:
# from rest_framework.decorators import api_view, authentication_classes
# @api_view(['GET'])
# @authentication_classes([FirebaseAuthentication])
# def protected_view(request):
# user = request.user # Authenticated user with tenant_id
2. Cloud IAM (Authorization)
Service Accounts:
# License API service account
Service Account: license-api@coditect-citus-prod.iam.gserviceaccount.com
Roles:
- roles/secretmanager.secretAccessor # Read secrets
- roles/cloudkms.signerVerifier # Sign/verify with KMS
- roles/cloudsql.client # Connect to Cloud SQL
- roles/logging.logWriter # Write logs
- roles/monitoring.metricWriter # Write metrics
# GKE node service account
Service Account: gke-node@coditect-citus-prod.iam.gserviceaccount.com
Roles:
- roles/logging.logWriter # Write logs
- roles/monitoring.metricWriter # Write metrics
- roles/storage.objectViewer # Pull container images (GCR)
# Cloud SQL service account (auto-created)
Service Account: cloudsql-client@coditect-citus-prod.iam.gserviceaccount.com
Roles:
- roles/cloudsql.client # Cloud SQL proxy
IAM Policy Example:
{
"bindings": [
{
"role": "roles/secretmanager.secretAccessor",
"members": [
"serviceAccount:license-api@coditect-citus-prod.iam.gserviceaccount.com"
],
"condition": {
"title": "License API secrets only",
"expression": "resource.name.startsWith('projects/coditect-citus-prod/secrets/db-') || resource.name.startsWith('projects/coditect-citus-prod/secrets/redis-')"
}
}
]
}
Workload Identity Configuration:
# Bind Kubernetes SA to Google SA
gcloud iam service-accounts add-iam-policy-binding \
license-api@coditect-citus-prod.iam.gserviceaccount.com \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:coditect-citus-prod.svc.id.goog[default/license-api-sa]"
# Annotate Kubernetes SA
kubectl annotate serviceaccount license-api-sa \
iam.gke.io/gcp-service-account=license-api@coditect-citus-prod.iam.gserviceaccount.com
3. Cloud KMS (License Signing)
Key Configuration:
resource "google_kms_key_ring" "license_keys" {
name = "coditect-license-keys"
location = "us-central1"
project = "coditect-citus-prod"
}
resource "google_kms_crypto_key" "license_signing_key" {
name = "license-signing-key"
key_ring = google_kms_key_ring.license_keys.id
purpose = "ASYMMETRIC_SIGN"
version_template {
algorithm = "RSA_SIGN_PKCS1_4096_SHA256"
protection_level = "HSM" # Hardware Security Module
}
rotation_period = "7776000s" # 90 days (automatic rotation)
lifecycle {
prevent_destroy = true
}
}
License Signing Process:
from google.cloud import kms
import hashlib
import base64
import json
async def sign_license(license_data: dict) -> str:
"""Sign license data with Cloud KMS RSA-4096 key."""
# 1. Serialize license data (deterministic ordering)
message = json.dumps(license_data, sort_keys=True).encode("utf-8")
# 2. Create SHA-256 digest
digest = hashlib.sha256(message).digest()
# 3. Sign with Cloud KMS
kms_client = kms.KeyManagementServiceAsyncClient()
key_name = (
"projects/coditect-citus-prod/locations/us-central1/"
"keyRings/coditect-license-keys/cryptoKeys/license-signing-key/"
"cryptoKeyVersions/1"
)
response = await kms_client.asymmetric_sign(
request={
"name": key_name,
"digest": {"sha256": digest}
}
)
# 4. Base64-encode signature
signature = base64.b64encode(response.signature).decode("utf-8")
# 5. Embed in JWT payload
jwt_payload = {
"license_data": license_data,
"signature": signature,
"key_version": "1",
"algorithm": "RSA_SIGN_PKCS1_4096_SHA256"
}
# 6. Return signed JWT
return create_jwt(jwt_payload)
Offline Verification (CODITECT Client):
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.backends import default_backend
import base64
import json
def verify_license_offline(jwt_token: str, public_key_pem: str) -> bool:
"""Verify license signature offline (no KMS call required)."""
# 1. Decode JWT
payload = decode_jwt(jwt_token)
license_data = payload["license_data"]
signature = base64.b64decode(payload["signature"])
# 2. Recreate message digest
message = json.dumps(license_data, sort_keys=True).encode("utf-8")
# 3. Load public key
public_key = serialization.load_pem_public_key(
public_key_pem.encode("utf-8"),
backend=default_backend()
)
# 4. Verify signature
try:
public_key.verify(
signature,
message,
padding.PKCS1v15(),
hashes.SHA256()
)
return True
except Exception:
return False
Public Key Distribution:
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from google.cloud import kms
from django.conf import settings
# Export public key from KMS
@api_view(['GET'])
@permission_classes([AllowAny])
def get_public_key(request):
"""Get public key for client-side license verification."""
kms_client = kms.KeyManagementServiceClient()
key_name = settings.KMS_KEY_NAME
response = kms_client.get_public_key(request={"name": key_name})
return Response({
"key_version": "1",
"algorithm": "RSA_SIGN_PKCS1_4096_SHA256",
"pem": response.pem
})
4. Secret Manager
Secrets Inventory:
# Database credentials
resource "google_secret_manager_secret" "db_password" {
secret_id = "db-password"
replication {
automatic = true # Replicate to all regions
}
}
resource "google_secret_manager_secret_version" "db_password_v1" {
secret = google_secret_manager_secret.db_password.id
secret_data = random_password.db_password.result
}
# Redis auth token
resource "google_secret_manager_secret" "redis_auth" {
secret_id = "redis-auth-token"
replication {
automatic = true
}
}
# Stripe API key
resource "google_secret_manager_secret" "stripe_api_key" {
secret_id = "stripe-api-key"
replication {
user_managed {
replicas {
location = "us-central1"
}
replicas {
location = "us-east1"
}
}
}
}
Access Control:
# Grant License API access to secrets
resource "google_secret_manager_secret_iam_member" "license_api_access" {
for_each = toset([
"db-password",
"db-app-user-password",
"redis-auth-token",
"stripe-api-key",
"sendgrid-api-key",
"jwt-secret-key"
])
secret_id = each.key
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:license-api@coditect-citus-prod.iam.gserviceaccount.com"
}
Fetching Secrets in Application:
from google.cloud import secretmanager
import os
async def get_secret(secret_id: str, version: str = "latest") -> str:
"""Fetch secret from Secret Manager."""
client = secretmanager.SecretManagerServiceAsyncClient()
name = f"projects/{PROJECT_ID}/secrets/{secret_id}/versions/{version}"
response = await client.access_secret_version(request={"name": name})
return response.payload.data.decode("UTF-8")
# Usage at application startup
async def load_secrets():
os.environ["DATABASE_PASSWORD"] = await get_secret("db-password")
os.environ["REDIS_AUTH_TOKEN"] = await get_secret("redis-auth-token")
os.environ["STRIPE_API_KEY"] = await get_secret("stripe-api-key")
os.environ["SENDGRID_API_KEY"] = await get_secret("sendgrid-api-key")
os.environ["JWT_SECRET_KEY"] = await get_secret("jwt-secret-key")
Secret Rotation:
# Manual rotation (generate new password)
NEW_PASSWORD=$(openssl rand -base64 32)
# Add new version to Secret Manager
echo -n "$NEW_PASSWORD" | gcloud secrets versions add db-password --data-file=-
# Update Cloud SQL password
gcloud sql users set-password app_user \
--instance=coditect-dev \
--password="$NEW_PASSWORD"
# Restart pods to pick up new secret
kubectl rollout restart deployment/license-api
5. PostgreSQL Row-Level Security (Multi-Tenant Isolation)
RLS Configuration:
-- Enable RLS on licenses table
ALTER TABLE licenses ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only see their own tenant's licenses
CREATE POLICY tenant_isolation ON licenses
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::UUID);
-- Grant access to app user
GRANT SELECT, INSERT, UPDATE, DELETE ON licenses TO app_user;
-- Prevent bypassing RLS (even for table owner)
ALTER TABLE licenses FORCE ROW LEVEL SECURITY;
Setting Tenant Context:
from sqlalchemy import event
from sqlalchemy.engine import Engine
@event.listens_for(Engine, "connect")
def set_search_path(dbapi_connection, connection_record):
"""Set tenant context on every connection."""
# Extract tenant_id from request context
tenant_id = get_current_tenant_id()
# Set PostgreSQL session variable
cursor = dbapi_connection.cursor()
cursor.execute("SET app.current_tenant = %s", (str(tenant_id),))
cursor.close()
# Usage in request handler
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from django.db import connection
from apps.licenses.models import License
from apps.licenses.serializers import AcquireLicenseRequestSerializer
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def acquire_license(request):
"""Acquire license with Row-Level Security filtering."""
# Validate request
serializer = AcquireLicenseRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# Tenant ID automatically set by JWT validation
current_user = request.user
tenant_id = current_user.tenant_id
# Set RLS context for this request
with connection.cursor() as cursor:
cursor.execute("SET app.current_tenant = %s", [str(tenant_id)])
# Query automatically filtered by RLS
# Only returns licenses for this tenant
licenses = License.objects.filter(
license_key=serializer.validated_data['license_key']
)
# RLS prevents seeing other tenants' data
Testing RLS:
-- As app_user
SET app.current_tenant = 'tenant-123';
SELECT * FROM licenses; -- Only returns tenant-123's licenses
SET app.current_tenant = 'tenant-456';
SELECT * FROM licenses; -- Only returns tenant-456's licenses
-- Attempt to bypass (fails)
SELECT * FROM licenses WHERE tenant_id = 'tenant-123';
-- Still filtered by RLS (only returns current tenant)
6. Hardware Fingerprinting (License Binding)
Fingerprint Generation:
import hashlib
import platform
import uuid
import subprocess
def generate_hardware_fingerprint() -> str:
"""Generate unique hardware fingerprint for license binding."""
components = []
# 1. CPU info
try:
if platform.system() == "Darwin":
cpu_info = subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"]).decode().strip()
elif platform.system() == "Linux":
with open("/proc/cpuinfo") as f:
cpu_info = [line for line in f if "model name" in line][0].split(":")[1].strip()
else:
cpu_info = platform.processor()
components.append(cpu_info)
except:
pass
# 2. MAC address (primary network interface)
try:
mac = ':'.join(['{:02x}'.format((uuid.getnode() >> i) & 0xff) for i in range(0,8*6,8)][::-1])
components.append(mac)
except:
pass
# 3. Disk serial (system drive)
try:
if platform.system() == "Darwin":
disk_serial = subprocess.check_output(
["system_profiler", "SPSerialATADataType"]
).decode()
# Extract serial from output
components.append(disk_serial[:100])
elif platform.system() == "Linux":
disk_serial = subprocess.check_output(
["lsblk", "-o", "SERIAL", "-d", "-n", "/dev/sda"]
).decode().strip()
components.append(disk_serial)
except:
pass
# 4. Hostname
components.append(platform.node())
# 5. OS info
components.append(f"{platform.system()}-{platform.release()}")
# Create SHA-256 hash
fingerprint_data = "|".join(components).encode("utf-8")
fingerprint_hash = hashlib.sha256(fingerprint_data).hexdigest()
return fingerprint_hash
License Binding:
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from django.db import transaction
import json
from apps.licenses.models import License
from apps.licenses.serializers import AcquireLicenseRequestSerializer
from apps.core.services import CloudKMSService
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def acquire_license(request):
"""Acquire license with hardware fingerprint binding."""
# Validate request
serializer = AcquireLicenseRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
license_key = serializer.validated_data['license_key']
hardware_fingerprint = serializer.validated_data['hardware_fingerprint']
# Validate license key exists
try:
license = License.objects.get(license_key=license_key)
except License.DoesNotExist:
return Response(
{"detail": "License not found"},
status=status.HTTP_404_NOT_FOUND
)
# Check hardware fingerprint
stored_fingerprints = license.hardware_fingerprints or []
if hardware_fingerprint not in stored_fingerprints:
# New device - check if seats available
if len(stored_fingerprints) >= license.max_seats:
return Response(
{"detail": "No available seats"},
status=status.HTTP_400_BAD_REQUEST
)
# Bind license to this device
with transaction.atomic():
stored_fingerprints.append(hardware_fingerprint)
license.hardware_fingerprints = stored_fingerprints
license.save()
# Create signed license token
kms_service = CloudKMSService()
license_data = {
"license_id": str(license.id),
"tenant_id": str(license.tenant_id),
"hardware_fingerprint": hardware_fingerprint,
"expires_at": license.expires_at.isoformat()
}
license_token = kms_service.sign_license_data(license_data)
return Response({"license_token": license_token})
7. Audit Logging (Compliance)
Cloud Audit Logs Types:
Admin Activity Logs (always enabled, no cost):
- IAM policy changes
- KMS key operations (create, destroy, rotate)
- Secret Manager access
- Firewall rule changes
- Retention: 400 days
Data Access Logs (optional, incurs cost):
- Cloud SQL queries (disabled for cost)
- Secret Manager reads (enabled)
- Cloud Storage access (N/A)
- Retention: 30 days
System Event Logs (always enabled, no cost):
- GKE node maintenance
- Cloud SQL failover
- Automatic backups
- Retention: 400 days
Policy Denied Logs (always enabled, no cost):
- IAM permission denied
- VPC firewall blocked
- Cloud Armor WAF blocked
- Retention: 400 days
Audit Log Query Examples:
-- All KMS signing operations
SELECT
timestamp,
protoPayload.authenticationInfo.principalEmail,
protoPayload.methodName,
protoPayload.resourceName
FROM `coditect-citus-prod.logs.cloudaudit_googleapis_com_activity`
WHERE protoPayload.serviceName = "cloudkms.googleapis.com"
AND protoPayload.methodName = "AsymmetricSign"
AND timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 24 HOUR)
ORDER BY timestamp DESC
-- Secret Manager access
SELECT
timestamp,
protoPayload.authenticationInfo.principalEmail,
protoPayload.resourceName,
protoPayload.methodName
FROM `coditect-citus-prod.logs.cloudaudit_googleapis_com_data_access`
WHERE protoPayload.serviceName = "secretmanager.googleapis.com"
AND protoPayload.methodName = "AccessSecretVersion"
AND timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
-- Failed authentication attempts
SELECT
timestamp,
httpRequest.remoteIp,
jsonPayload.error
FROM `coditect-citus-prod.logs.requests`
WHERE httpRequest.status = 401
AND timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)
ORDER BY timestamp DESC
Application-Level Audit Logging:
# PostgreSQL audit table
CREATE TABLE audit_logs (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL,
user_id UUID,
action VARCHAR(100) NOT NULL,
resource_type VARCHAR(50),
resource_id VARCHAR(100),
details JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
# Audit logging function
async def log_audit_event(
action: str,
tenant_id: str,
user_id: str = None,
resource_type: str = None,
resource_id: str = None,
details: dict = None,
request: Request = None
):
await db.execute(
"""
INSERT INTO audit_logs
(tenant_id, user_id, action, resource_type, resource_id, details, ip_address, user_agent)
VALUES (:tenant_id, :user_id, :action, :resource_type, :resource_id, :details, :ip, :ua)
""",
{
"tenant_id": tenant_id,
"user_id": user_id,
"action": action,
"resource_type": resource_type,
"resource_id": resource_id,
"details": json.dumps(details) if details else None,
"ip": request.client.host if request else None,
"ua": request.headers.get("User-Agent") if request else None
}
)
# Usage
await log_audit_event(
action="license.acquire",
tenant_id=tenant_id,
user_id=user_id,
resource_type="license",
resource_id=license_id,
details={"hardware_fingerprint": fp, "seats_used": seats_used},
request=request
)
8. Encryption at Rest & In Transit
Encryption at Rest:
Cloud SQL:
Encryption: Customer-Managed Encryption Key (CMEK)
Key: projects/coditect-citus-prod/locations/us-central1/keyRings/sql-keys/cryptoKeys/sql-key
Algorithm: AES-256-GCM
Key Rotation: Automatic (90 days)
Cloud Memorystore Redis:
Encryption: Google-managed encryption keys (automatic)
Algorithm: AES-256
Key Rotation: Automatic
Persistent Disks (GKE):
Encryption: Customer-Managed Encryption Key (CMEK)
Key: projects/coditect-citus-prod/locations/us-central1/keyRings/disk-keys/cryptoKeys/disk-key
Algorithm: AES-256-XTS
Key Rotation: Automatic (90 days)
Secret Manager:
Encryption: Google-managed encryption keys (automatic)
Algorithm: AES-256-GCM
Key Rotation: Automatic
Encryption in Transit:
Load Balancer → Ingress:
Protocol: HTTPS (TLS 1.3)
Certificate: Let's Encrypt (auto-renewed)
Cipher Suites: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384
Application → Cloud SQL:
Protocol: SSL/TLS (PostgreSQL native)
Certificate: Google-managed CA
Verification: Require SSL
Application → Redis:
Protocol: TLS (Memorystore in-transit encryption)
Mode: SERVER_AUTHENTICATION
Certificate: Google-managed
Pod → Pod (Future):
Protocol: mTLS (Istio service mesh)
Certificate: Automatically rotated
mTLS Mode: STRICT (enforced)
Security Incident Response
Incident Detection
Automated Alerts (PagerDuty):
Critical Alerts (P1 - Immediate):
- Multiple failed authentication attempts (>10/min)
- Unauthorized KMS signing attempts
- Secret Manager access from unexpected service account
- Cloud SQL connection from unknown IP
- Unusual API traffic spike (>5x normal)
High Alerts (P2 - 15 minutes):
- Cloud Armor WAF blocks (>100/min)
- Database query errors (>50/min)
- License validation failures (>10/min)
- Redis connection failures
Medium Alerts (P3 - 1 hour):
- Certificate expiration (< 30 days)
- Disk usage (>80%)
- High CPU usage (>90% for 10 minutes)
Incident Playbooks
Scenario 1: Compromised API Key
# 1. Rotate secret immediately
gcloud secrets versions add stripe-api-key --data-file=new_key.txt
# 2. Restart affected pods
kubectl rollout restart deployment/license-api
# 3. Revoke old key in Stripe dashboard
# 4. Review audit logs for unauthorized usage
gcloud logging read "resource.type=secret_manager AND \
protoPayload.resourceName=projects/coditect-citus-prod/secrets/stripe-api-key" \
--format=json --limit=100
# 5. File incident report
Scenario 2: Unusual License Activity
-- Identify suspicious license acquisitions
SELECT
l.license_key,
COUNT(*) as acquisition_count,
COUNT(DISTINCT hardware_fingerprint) as unique_devices,
ARRAY_AGG(DISTINCT ip_address) as ip_addresses
FROM audit_logs
WHERE action = 'license.acquire'
AND created_at >= NOW() - INTERVAL '1 hour'
GROUP BY l.license_key
HAVING COUNT(DISTINCT hardware_fingerprint) > max_seats * 2
ORDER BY acquisition_count DESC;
-- Temporarily suspend license
UPDATE licenses SET status = 'suspended' WHERE license_key = 'suspicious-key';
-- Notify customer
-- Investigate further
Compliance & Certifications
GDPR Compliance
Data Subject Rights:
# Right to access (export user data)
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from apps.accounts.models import User
from apps.licenses.models import License, AuditLog
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def export_user_data(request):
"""Export all user data for GDPR compliance (right to access)."""
user = request.user
tenant_id = user.tenant_id
# Export all user data
user_data = {
"id": str(user.id),
"email": user.email,
"created_at": user.created_at.isoformat(),
"last_login": user.last_login.isoformat() if user.last_login else None,
}
licenses = License.objects.filter(tenant_id=tenant_id).values(
'id', 'tier', 'max_seats', 'expiry_date', 'created_at'
)
audit_logs = AuditLog.objects.filter(user_id=user.id).values(
'id', 'action', 'resource_type', 'resource_id', 'timestamp'
)
data = {
"user": user_data,
"licenses": list(licenses),
"audit_logs": list(audit_logs)
}
return Response(data, status=status.HTTP_200_OK)
# Right to erasure (delete user data)
from django.db import transaction
@api_view(['DELETE'])
@permission_classes([IsAuthenticated])
def delete_user_data(request):
"""Delete user data for GDPR compliance (right to erasure)."""
user = request.user
with transaction.atomic():
# Anonymize audit logs (retain for compliance)
AuditLog.objects.filter(user_id=user.id).update(user_id=None)
# Delete user account
user.delete()
return Response(
{"status": "deleted", "user_id": str(user.id)},
status=status.HTTP_200_OK
)
SOC 2 Type II
Control Requirements:
Access Controls:
- MFA for all administrative access (future)
- Role-based access control (RBAC)
- Least privilege IAM policies
- Workload Identity (no long-lived keys)
Audit Logging:
- All admin actions logged (Cloud Audit Logs)
- All data access logged (application-level)
- Logs retained for 7 years (compliance)
- Tamper-proof logging (Cloud Logging)
Encryption:
- Encryption at rest (CMEK)
- Encryption in transit (TLS 1.3)
- Key rotation (automatic, 90 days)
- HSM-backed keys (Cloud KMS)
Change Management:
- All infrastructure changes via OpenTofu
- Code review required (GitHub PRs)
- Automated testing (CI/CD)
- Rollback procedures documented
Security Best Practices Checklist
Authentication & Authorization
- OAuth2 with JWT tokens (RS256)
- Workload Identity (no long-lived keys)
- Hardware fingerprinting (license binding)
- Multi-factor authentication (future)
- Session expiration (1-hour access token)
- PostgreSQL RLS (tenant isolation)
Encryption
- TLS 1.3 everywhere
- Cloud KMS (HSM-backed keys)
- CMEK for Cloud SQL, persistent disks
- License signing (RSA-4096)
- mTLS for pod-to-pod (future)
Secrets Management
- Secret Manager (no secrets in code)
- IAM access control
- Automatic replication
- Version management
- Automatic rotation (future)
Audit & Compliance
- Cloud Audit Logs (400-day retention)
- Application audit logs (7-year retention)
- GDPR compliance (data export/delete)
- Data residency (us-central1)
- SOC 2 certification (in progress)
Network Security
- Cloud Armor (WAF + DDoS)
- Private GKE cluster
- VPC firewall rules
- No public database IPs
- Rate limiting (100 req/min)
Related Diagrams
- C1: System Context - External system view
- C2: Container Diagram - High-level containers
- C3: GKE Components - Kubernetes internals
- C3: Networking Components - VPC and networking
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2025-11-23 | SDD Architect | Initial security component diagram |
Document Classification: Internal - Architecture Documentation Review Cycle: Quarterly (or upon security changes) Next Review Date: 2026-02-23