Custom Domain and SSL Configuration
Executive Summary
This document specifies the architecture, implementation, and operational procedures for custom domain and SSL/TLS configuration for the BIO-QMS documentation platform. The configuration provides secure, production-grade HTTPS access via bio-qms.docs.coditect.ai with Google-managed certificates, automated renewal, comprehensive security headers, and compliance with regulated data transmission requirements.
Key Objectives:
- Secure domain access via
bio-qms.docs.coditect.ai - Automated SSL/TLS certificate provisioning and renewal
- Zero-downtime certificate rotation
- Comprehensive security headers and HSTS policy
- Scalable multi-project domain pattern
- Compliance with 21 CFR Part 11 data transmission requirements
Target Infrastructure:
- GCP Cloud DNS for authoritative name resolution
- GCP Load Balancer with HTTPS termination
- Google-managed SSL certificates with auto-renewal
- Cloud CDN for edge caching and TLS optimization
- Cloud Run backend services
1. Domain Architecture Overview
1.1 Traffic Flow
User Browser
↓
DNS Resolution (Cloud DNS: bio-qms.docs.coditect.ai)
↓
Global Load Balancer (HTTPS Termination, TLS 1.3)
↓
Cloud CDN (Edge Caching, OCSP Stapling)
↓
Cloud Run Service (Backend: port 8080)
1.2 Component Responsibilities
| Component | Responsibility |
|---|---|
| Cloud DNS | Authoritative DNS zone for docs.coditect.ai, A/AAAA records pointing to load balancer IP |
| Load Balancer | HTTPS frontend, certificate attachment, HTTP→HTTPS redirect, URL mapping |
| SSL Certificate | Google-managed certificate for bio-qms.docs.coditect.ai, auto-renewal every 90 days |
| Cloud CDN | Edge caching, TLS handshake optimization, OCSP stapling |
| Cloud Run | Backend service, receives HTTP traffic from load balancer internal IP |
| Security Headers | HSTS, CSP, X-Frame-Options injected at load balancer or Cloud Run response level |
1.3 High Availability Design
- Multi-region DNS: Cloud DNS anycast across Google's global network
- Load balancer global: Single anycast IP, automatic failover across regions
- Certificate redundancy: Google stores certificate in multiple datacenters
- CDN edge locations: 100+ global PoPs for low-latency TLS handshake
- RTO/RPO: Recovery Time Objective <5 minutes, Recovery Point Objective = 0 (no data loss)
2. Domain Naming Decision
2.1 Selected Approach: Subdomain Pattern
Domain: bio-qms.docs.coditect.ai
Rationale:
- Project isolation: Each project gets dedicated subdomain (
{project}.docs.coditect.ai) - Wildcard certificate: Single cert covers
*.docs.coditect.aifor future projects - Independent deployment: Changes to BIO-QMS don't affect other projects
- SEO optimization: Project-specific subdomain improves search indexing
- Cookie isolation: Prevents cookie leakage between projects
2.2 Alternative Approaches (Not Selected)
Option A: Path-Based Routing (docs.coditect.ai/bio-qms)
Pros:
- Single certificate for
docs.coditect.ai - Unified analytics and monitoring
- Simpler DNS management
Cons:
- Shared backend service (scaling conflicts)
- Path rewriting complexity at load balancer
- Cookie sharing across projects (security risk)
- URL migration pain when extracting project to subdomain
Verdict: Rejected due to scaling and isolation concerns.
Option B: Apex Domain (bio-qms.coditect.ai)
Pros:
- Maximum project independence
- Clearest branding
Cons:
- Certificate proliferation (one per project)
- DNS zone proliferation (operational overhead)
- No visual indicator that it's documentation
Verdict: Rejected to maintain docs.* namespace consistency.
2.3 Future Multi-Project Pattern
bio-qms.docs.coditect.ai → BIO-QMS Documentation
pharma-mes.docs.coditect.ai → Pharma MES Documentation
lab-lims.docs.coditect.ai → Lab LIMS Documentation
*.docs.coditect.ai → Wildcard SSL certificate
Wildcard Certificate: *.docs.coditect.ai covers all projects, single renewal process.
Load Balancer URL Map:
urlMap:
hostRules:
- hosts: ["bio-qms.docs.coditect.ai"]
pathMatcher: bio-qms-backend
- hosts: ["pharma-mes.docs.coditect.ai"]
pathMatcher: pharma-mes-backend
defaultService: 404-backend
3. DNS Configuration Specification
3.1 Cloud DNS Managed Zone
Zone Name: docs-coditect-ai
DNS Name: docs.coditect.ai.
DNSSEC: Enabled (see Section 9)
Zone Creation:
gcloud dns managed-zones create docs-coditect-ai \
--dns-name="docs.coditect.ai." \
--description="CODITECT Documentation Platform - Multi-Project Zone" \
--visibility=public \
--dnssec-state=on
3.2 DNS Records
3.2.1 A and AAAA Records for Load Balancer
A Record:
bio-qms.docs.coditect.ai. 300 IN A <LOAD_BALANCER_IP>
AAAA Record (IPv6):
bio-qms.docs.coditect.ai. 300 IN AAAA <LOAD_BALANCER_IPV6>
TTL Rationale: 300 seconds (5 minutes) balances DNS propagation speed with cache efficiency.
gcloud Command:
# Get load balancer IP
LB_IP=$(gcloud compute addresses describe bio-qms-docs-ip --global --format="value(address)")
# Add A record
gcloud dns record-sets create bio-qms.docs.coditect.ai. \
--zone=docs-coditect-ai \
--type=A \
--ttl=300 \
--rrdatas="${LB_IP}"
# Add AAAA record (IPv6)
LB_IPV6=$(gcloud compute addresses describe bio-qms-docs-ipv6 --global --format="value(address)")
gcloud dns record-sets create bio-qms.docs.coditect.ai. \
--zone=docs-coditect-ai \
--type=AAAA \
--ttl=300 \
--rrdatas="${LB_IPV6}"
3.2.2 CNAME for www Redirect (Optional)
www.bio-qms.docs.coditect.ai. 300 IN CNAME bio-qms.docs.coditect.ai.
Purpose: Redirect www variant to canonical domain (load balancer enforces redirect to non-www).
3.2.3 CAA Record for Certificate Authority Restriction
bio-qms.docs.coditect.ai. 3600 IN CAA 0 issue "pki.goog"
bio-qms.docs.coditect.ai. 3600 IN CAA 0 issuewild "pki.goog"
Purpose: Restrict certificate issuance to Google Trust Services only (see Section 8).
gcloud Command:
gcloud dns record-sets create bio-qms.docs.coditect.ai. \
--zone=docs-coditect-ai \
--type=CAA \
--ttl=3600 \
--rrdatas='0 issue "pki.goog"','0 issuewild "pki.goog"'
3.3 Delegation from Parent Zone
Parent Zone: coditect.ai (assumed registered with domain registrar)
NS Records at Registrar:
docs.coditect.ai. IN NS ns-cloud-a1.googledomains.com.
docs.coditect.ai. IN NS ns-cloud-a2.googledomains.com.
docs.coditect.ai. IN NS ns-cloud-a3.googledomains.com.
docs.coditect.ai. IN NS ns-cloud-a4.googledomains.com.
Retrieve Name Servers:
gcloud dns managed-zones describe docs-coditect-ai --format="value(nameServers)"
Manual Step: Add NS records at domain registrar (e.g., Google Domains, Namecheap).
4. SSL/TLS Certificate Specification
4.1 Google-Managed Certificate
Certificate Type: Google-managed SSL certificate
Domains Covered: bio-qms.docs.coditect.ai (single-domain initially)
Issuer: Google Trust Services (GTS)
Validity Period: 90 days (auto-renewed 30 days before expiration)
Certificate Authority: Let's Encrypt via Google's managed service
Future Wildcard Certificate:
Domains: *.docs.coditect.ai, docs.coditect.ai
4.2 Certificate Creation
gcloud Command:
gcloud compute ssl-certificates create bio-qms-docs-cert \
--description="SSL certificate for bio-qms.docs.coditect.ai" \
--domains=bio-qms.docs.coditect.ai \
--global
Terraform Configuration:
resource "google_compute_managed_ssl_certificate" "bio_qms_docs" {
name = "bio-qms-docs-cert"
description = "SSL certificate for bio-qms.docs.coditect.ai"
managed {
domains = ["bio-qms.docs.coditect.ai"]
}
}
4.3 Wildcard Certificate (Future)
gcloud Command:
gcloud compute ssl-certificates create docs-coditect-ai-wildcard \
--description="Wildcard SSL certificate for *.docs.coditect.ai" \
--domains="*.docs.coditect.ai,docs.coditect.ai" \
--global
Terraform Configuration:
resource "google_compute_managed_ssl_certificate" "docs_wildcard" {
name = "docs-coditect-ai-wildcard"
description = "Wildcard SSL certificate for *.docs.coditect.ai"
managed {
domains = [
"*.docs.coditect.ai",
"docs.coditect.ai"
]
}
}
5. Certificate Provisioning Process
5.1 Provisioning Timeline
| Phase | Duration | Description |
|---|---|---|
| 1. Certificate Creation | Instant | Create managed certificate resource in GCP |
| 2. Load Balancer Attachment | Instant | Attach certificate to HTTPS frontend |
| 3. DNS Propagation | 5-60 min | A/AAAA records propagate globally |
| 4. Domain Verification | 5-30 min | Google verifies DNS ownership via HTTP-01 challenge |
| 5. Certificate Issuance | 10-60 min | Google Trust Services issues certificate |
| 6. Certificate Activation | Instant | Certificate becomes active on load balancer |
| Total | 20-150 min | Typical: 30-60 minutes |
5.2 Domain Verification Methods
Google-Managed Certificates use HTTP-01 challenge:
- User creates certificate with domain
bio-qms.docs.coditect.ai - User attaches certificate to load balancer HTTPS frontend
- User points DNS A record to load balancer IP
- Google sends HTTP request to
http://bio-qms.docs.coditect.ai/.well-known/acme-challenge/<token> - Load balancer responds with verification token
- Google verifies ownership and issues certificate
Prerequisites:
- DNS A/AAAA records must point to load balancer IP before provisioning
- Load balancer must respond to HTTP requests (port 80) for verification
- Firewall rules must allow HTTP traffic during verification
5.3 Verification Status Monitoring
gcloud Command:
gcloud compute ssl-certificates describe bio-qms-docs-cert \
--global \
--format="value(managed.status, managed.domainStatus)"
Status Values:
PROVISIONING: Certificate creation in progressACTIVE: Certificate issued and deployedRENEWAL_FAILED: Auto-renewal failed (requires manual intervention)
Domain-Specific Status:
gcloud compute ssl-certificates describe bio-qms-docs-cert \
--global \
--format="table(managed.domainStatus.bio-qms.docs.coditect.ai)"
Domain Status Values:
PROVISIONING: Verification in progressACTIVE: Domain verified and certificate issuedFAILED_NOT_VISIBLE: DNS not propagated or load balancer not accessibleFAILED_CAA_FORBIDDEN: CAA record blocks issuance (check CAA records)FAILED_RATE_LIMITED: Let's Encrypt rate limit hit (wait 7 days)
5.4 Troubleshooting Provisioning Failures
5.4.1 FAILED_NOT_VISIBLE
Cause: DNS not pointing to load balancer, or load balancer not responding to HTTP.
Resolution:
# Verify DNS resolution
dig bio-qms.docs.coditect.ai +short
# Should return load balancer IP
# Test HTTP connectivity
curl -v http://bio-qms.docs.coditect.ai/.well-known/acme-challenge/test
# Should return 404 (not 5xx or timeout)
# Check load balancer health
gcloud compute backend-services get-health bio-qms-docs-backend --global
5.4.2 FAILED_CAA_FORBIDDEN
Cause: CAA record restricts issuance to non-Google CA.
Resolution:
# Check CAA records
dig bio-qms.docs.coditect.ai CAA +short
# Should include: 0 issue "pki.goog"
# Update CAA record to allow Google
gcloud dns record-sets update bio-qms.docs.coditect.ai. \
--zone=docs-coditect-ai \
--type=CAA \
--ttl=3600 \
--rrdatas='0 issue "pki.goog"'
5.4.3 FAILED_RATE_LIMITED
Cause: Let's Encrypt rate limit (50 certificates per week per domain).
Resolution:
- Wait 7 days for rate limit reset
- Use wildcard certificate to reduce certificate count
- Contact Let's Encrypt support for rate limit increase (enterprise plans)
6. TLS Configuration
6.1 Protocol Versions
Minimum Version: TLS 1.2 Preferred Version: TLS 1.3
Rationale:
- TLS 1.0/1.1 deprecated (RFC 8996)
- TLS 1.2 required for legacy browser compatibility (<1% traffic)
- TLS 1.3 offers 1-RTT handshake (40% faster than TLS 1.2)
GCP Load Balancer TLS Policy:
gcloud compute ssl-policies create bio-qms-tls-policy \
--profile=MODERN \
--min-tls-version=1.2
TLS Policy Profiles:
COMPATIBLE: TLS 1.0+ (not recommended)MODERN: TLS 1.2+ (recommended)RESTRICTED: TLS 1.2+ with strong ciphers only
Terraform Configuration:
resource "google_compute_ssl_policy" "bio_qms_tls" {
name = "bio-qms-tls-policy"
profile = "MODERN"
min_tls_version = "TLS_1_2"
}
resource "google_compute_target_https_proxy" "bio_qms_docs" {
name = "bio-qms-docs-https-proxy"
url_map = google_compute_url_map.bio_qms_docs.id
ssl_certificates = [google_compute_managed_ssl_certificate.bio_qms_docs.id]
ssl_policy = google_compute_ssl_policy.bio_qms_tls.id
}
6.2 Cipher Suite Selection
GCP MODERN Profile Ciphers (TLS 1.2):
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS 1.3 Ciphers (Preferred):
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
Security Properties:
- Forward secrecy via ECDHE key exchange
- AEAD ciphers only (GCM, CHACHA20-POLY1305)
- No CBC-mode ciphers (mitigates BEAST, Lucky13)
- No RC4, 3DES, MD5, SHA1 in signatures
6.3 Certificate Chain Configuration
Chain Order:
1. Leaf Certificate (bio-qms.docs.coditect.ai)
↓
2. Intermediate Certificate (GTS CA 1C3)
↓
3. Root Certificate (GTS Root R1)
Google-managed certificates automatically include full chain.
Verification:
echo | openssl s_client -connect bio-qms.docs.coditect.ai:443 -servername bio-qms.docs.coditect.ai 2>/dev/null | openssl x509 -text -noout
7. HTTP to HTTPS Redirect
7.1 Load Balancer HTTP Frontend
Purpose: Accept HTTP traffic on port 80, redirect to HTTPS.
URL Map Configuration:
urlMap:
defaultUrlRedirect:
httpsRedirect: true
redirectResponseCode: MOVED_PERMANENTLY_DEFAULT # 301
gcloud Command:
# Create URL map with HTTP→HTTPS redirect
gcloud compute url-maps create bio-qms-docs-http-redirect \
--default-url-redirect-https-redirect
# Create HTTP proxy
gcloud compute target-http-proxies create bio-qms-docs-http-proxy \
--url-map=bio-qms-docs-http-redirect
# Create forwarding rule (HTTP listener)
gcloud compute forwarding-rules create bio-qms-docs-http-rule \
--global \
--target-http-proxy=bio-qms-docs-http-proxy \
--address=bio-qms-docs-ip \
--ports=80
Terraform Configuration:
# URL map for HTTP→HTTPS redirect
resource "google_compute_url_map" "bio_qms_http_redirect" {
name = "bio-qms-docs-http-redirect"
default_url_redirect {
https_redirect = true
redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
strip_query = false
}
}
# HTTP proxy
resource "google_compute_target_http_proxy" "bio_qms_docs" {
name = "bio-qms-docs-http-proxy"
url_map = google_compute_url_map.bio_qms_http_redirect.id
}
# Forwarding rule (HTTP listener on port 80)
resource "google_compute_global_forwarding_rule" "bio_qms_http" {
name = "bio-qms-docs-http-rule"
target = google_compute_target_http_proxy.bio_qms_docs.id
ip_address = google_compute_global_address.bio_qms_docs.address
port_range = "80"
}
7.2 Redirect Behavior
Request:
GET http://bio-qms.docs.coditect.ai/path?query=value
Host: bio-qms.docs.coditect.ai
Response:
HTTP/1.1 301 Moved Permanently
Location: https://bio-qms.docs.coditect.ai/path?query=value
Preservation:
- Path preserved (
/path) - Query string preserved (
?query=value) - Fragment preserved (client-side, not sent to server)
7.3 www to non-www Redirect
Option A: DNS CNAME + Load Balancer Host Redirect
DNS:
www.bio-qms.docs.coditect.ai. 300 IN CNAME bio-qms.docs.coditect.ai.
URL Map Host Redirect:
hostRules:
- hosts: ["www.bio-qms.docs.coditect.ai"]
pathMatcher: www-redirect
pathMatchers:
- name: www-redirect
defaultUrlRedirect:
hostRedirect: bio-qms.docs.coditect.ai
redirectResponseCode: MOVED_PERMANENTLY_DEFAULT
Option B: Application-Level Redirect (Cloud Run)
Middleware (Go example):
func WWWRedirectMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.Host, "www.") {
target := "https://" + strings.TrimPrefix(r.Host, "www.") + r.URL.Path
if r.URL.RawQuery != "" {
target += "?" + r.URL.RawQuery
}
http.Redirect(w, r, target, http.StatusMovedPermanently)
return
}
next.ServeHTTP(w, r)
})
}
8. HSTS (HTTP Strict Transport Security)
8.1 HSTS Policy
Header:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Parameters:
max-age=31536000: 1 year (365 days in seconds)includeSubDomains: Apply policy to all subdomains ofbio-qms.docs.coditect.aipreload: Eligible for browser HSTS preload list
Purpose:
- Prevent SSL stripping attacks (force HTTPS for all requests)
- Prevent certificate warnings bypass
- Improve performance (skip HTTP→HTTPS redirect after first visit)
8.2 HSTS Implementation
Cloud Run Response Headers (Go example):
func HSTSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
next.ServeHTTP(w, r)
})
}
Load Balancer Custom Headers (Terraform):
resource "google_compute_backend_service" "bio_qms_docs" {
name = "bio-qms-docs-backend"
# ... other config ...
custom_response_headers = [
"Strict-Transport-Security: max-age=31536000; includeSubDomains; preload"
]
}
8.3 HSTS Preload Submission
Prerequisites:
- HSTS header served on all requests (including HTTP redirects)
max-age≥ 31536000 (1 year)includeSubDomainsdirective presentpreloaddirective present- Redirect HTTP to HTTPS on same host
- Serve HSTS header on base domain (not just subdomain)
Submission Process:
- Verify prerequisites at hstspreload.org
- Submit domain
docs.coditect.ai(base domain, not subdomain) - Wait 2-3 months for inclusion in Chromium preload list
- Preload list distributed to Chrome, Firefox, Safari, Edge
Testing:
curl -I https://bio-qms.docs.coditect.ai | grep -i strict-transport-security
9. Security Headers
9.1 Comprehensive Security Header Suite
Response Headers:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
X-XSS-Protection: 1; mode=block
9.2 Content Security Policy (CSP)
Policy Breakdown:
| Directive | Value | Purpose |
|---|---|---|
default-src | 'self' | Default: load resources only from same origin |
script-src | 'self' 'unsafe-inline' | Allow inline scripts (required for MkDocs Material theme) |
style-src | 'self' 'unsafe-inline' | Allow inline styles (required for MkDocs Material theme) |
img-src | 'self' data: | Allow images from same origin + data URIs (SVG icons) |
font-src | 'self' | Load fonts from same origin only |
connect-src | 'self' | Allow AJAX/WebSocket to same origin only |
frame-ancestors | 'none' | Prevent clickjacking (equivalent to X-Frame-Options: DENY) |
Future Refinement (Remove unsafe-inline):
script-src 'self' 'nonce-<random>' 'sha256-<hash>'
style-src 'self' 'nonce-<random>' 'sha256-<hash>'
Nonce Generation (Go example):
func generateCSPNonce() string {
b := make([]byte, 16)
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
}
func CSPMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nonce := generateCSPNonce()
csp := fmt.Sprintf("default-src 'self'; script-src 'self' 'nonce-%s'; style-src 'self' 'nonce-%s'", nonce, nonce)
w.Header().Set("Content-Security-Policy", csp)
// Inject nonce into template context
ctx := context.WithValue(r.Context(), "csp-nonce", nonce)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
9.3 X-Frame-Options
Header:
X-Frame-Options: DENY
Purpose: Prevent clickjacking by disallowing embedding in <iframe>.
Alternatives:
SAMEORIGIN: Allow framing by same origin onlyALLOW-FROM https://example.com: Allow specific origin (deprecated, use CSPframe-ancestors)
CSP Equivalent (Preferred):
Content-Security-Policy: frame-ancestors 'none'
9.4 X-Content-Type-Options
Header:
X-Content-Type-Options: nosniff
Purpose: Prevent MIME-type sniffing attacks (force browser to respect Content-Type header).
Example Attack Prevention:
- User uploads
malicious.jpg(actually HTML with<script>) - Without
nosniff, browser might execute as HTML ifContent-Typemissing - With
nosniff, browser only renders as image
9.5 Referrer-Policy
Header:
Referrer-Policy: strict-origin-when-cross-origin
Behavior:
- Same-origin requests: Send full referrer URL (
https://bio-qms.docs.coditect.ai/page-a→https://bio-qms.docs.coditect.ai/page-b) - Cross-origin HTTPS→HTTPS: Send origin only (
https://bio-qms.docs.coditect.ai→https://external.com) - Cross-origin HTTPS→HTTP: Send no referrer (downgrade protection)
Alternatives:
no-referrer: Never send referrer (breaks analytics)same-origin: Send referrer only for same-origin requestsstrict-origin: Send origin only for cross-origin HTTPS requests
9.6 Permissions-Policy
Header:
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=()
Purpose: Disable unnecessary browser APIs.
Format: <feature>=(<allowed-origins>)
- Empty
(): Disable for all origins self: Allow for same origin onlyhttps://example.com: Allow for specific origin
9.7 X-XSS-Protection (Legacy)
Header:
X-XSS-Protection: 1; mode=block
Purpose: Enable browser XSS filter (legacy, superseded by CSP).
Status: Deprecated in modern browsers (Chrome, Firefox removed filter), but harmless to include for older browsers.
10. Multi-Project Domain Pattern
10.1 Future Project Domains
| Project | Domain | Certificate |
|---|---|---|
| BIO-QMS | bio-qms.docs.coditect.ai | Covered by *.docs.coditect.ai |
| Pharma MES | pharma-mes.docs.coditect.ai | Covered by *.docs.coditect.ai |
| Lab LIMS | lab-lims.docs.coditect.ai | Covered by *.docs.coditect.ai |
| Platform Docs | platform.docs.coditect.ai | Covered by *.docs.coditect.ai |
10.2 Wildcard Certificate Migration
Current State:
- Single-domain certificate:
bio-qms.docs.coditect.ai
Target State:
- Wildcard certificate:
*.docs.coditect.ai+docs.coditect.ai
Migration Steps:
- Create Wildcard Certificate:
gcloud compute ssl-certificates create docs-coditect-ai-wildcard \
--domains="*.docs.coditect.ai,docs.coditect.ai" \
--global
- Attach to Existing Load Balancer (Zero-Downtime):
# Add wildcard cert alongside existing cert
gcloud compute target-https-proxies update bio-qms-docs-https-proxy \
--ssl-certificates=bio-qms-docs-cert,docs-coditect-ai-wildcard
# Wait for wildcard cert to become ACTIVE (30-60 min)
watch gcloud compute ssl-certificates describe docs-coditect-ai-wildcard --global --format="value(managed.status)"
# Remove single-domain cert
gcloud compute target-https-proxies update bio-qms-docs-https-proxy \
--ssl-certificates=docs-coditect-ai-wildcard
- Delete Old Certificate (After 30 Days):
gcloud compute ssl-certificates delete bio-qms-docs-cert --global
10.3 Load Balancer URL Map for Multi-Project
URL Map with Host-Based Routing:
urlMap:
name: docs-coditect-ai-urlmap
hostRules:
- hosts: ["bio-qms.docs.coditect.ai"]
pathMatcher: bio-qms-backend
- hosts: ["pharma-mes.docs.coditect.ai"]
pathMatcher: pharma-mes-backend
- hosts: ["lab-lims.docs.coditect.ai"]
pathMatcher: lab-lims-backend
pathMatchers:
- name: bio-qms-backend
defaultService: bio-qms-docs-backend-service
- name: pharma-mes-backend
defaultService: pharma-mes-docs-backend-service
- name: lab-lims-backend
defaultService: lab-lims-docs-backend-service
defaultService: 404-backend-service
Terraform Configuration:
resource "google_compute_url_map" "docs_coditect_ai" {
name = "docs-coditect-ai-urlmap"
default_service = google_compute_backend_service.not_found.id
host_rule {
hosts = ["bio-qms.docs.coditect.ai"]
path_matcher = "bio-qms"
}
host_rule {
hosts = ["pharma-mes.docs.coditect.ai"]
path_matcher = "pharma-mes"
}
path_matcher {
name = "bio-qms"
default_service = google_compute_backend_service.bio_qms_docs.id
}
path_matcher {
name = "pharma-mes"
default_service = google_compute_backend_service.pharma_mes_docs.id
}
}
11. GCP Load Balancer Configuration
11.1 Architecture Components
┌─────────────────────────────────────────────────────────────┐
│ Global HTTP(S) Load Balancer │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Forwarding Rule │ │ Forwarding Rule │ │
│ │ (HTTP:80) │ │ (HTTPS:443) │ │
│ │ IP: <GLOBAL_IP> │ │ IP: <GLOBAL_IP> │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ v v │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ HTTP Proxy │ │ HTTPS Proxy │ │
│ │ (Redirect) │ │ + SSL Cert │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ v v │
│ ┌──────────────────────────────────────┐ │
│ │ URL Map (Host-Based Routing) │ │
│ │ - bio-qms.docs → Backend Service A │ │
│ │ - pharma.docs → Backend Service B │ │
│ └────────┬──────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────┐ │
│ │ Backend Service │ │
│ │ + Cloud CDN │ │
│ │ + Health Check │ │
│ └────────┬─────────┘ │
│ │ │
│ v │
│ ┌──────────────────┐ │
│ │ NEG (Serverless) │ │
│ │ → Cloud Run │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
11.2 Component Creation
11.2.1 Reserve Global IP Address
# IPv4
gcloud compute addresses create bio-qms-docs-ip \
--ip-version=IPV4 \
--global
# IPv6
gcloud compute addresses create bio-qms-docs-ipv6 \
--ip-version=IPV6 \
--global
# Retrieve addresses
gcloud compute addresses describe bio-qms-docs-ip --global --format="value(address)"
gcloud compute addresses describe bio-qms-docs-ipv6 --global --format="value(address)"
Terraform:
resource "google_compute_global_address" "bio_qms_docs_ipv4" {
name = "bio-qms-docs-ip"
ip_version = "IPV4"
}
resource "google_compute_global_address" "bio_qms_docs_ipv6" {
name = "bio-qms-docs-ip-ipv6"
ip_version = "IPV6"
}
11.2.2 Serverless NEG for Cloud Run
gcloud compute network-endpoint-groups create bio-qms-docs-neg \
--region=us-central1 \
--network-endpoint-type=SERVERLESS \
--cloud-run-service=bio-qms-docs
Terraform:
resource "google_compute_region_network_endpoint_group" "bio_qms_docs" {
name = "bio-qms-docs-neg"
region = "us-central1"
network_endpoint_type = "SERVERLESS"
cloud_run {
service = google_cloud_run_service.bio_qms_docs.name
}
}
11.2.3 Backend Service with Cloud CDN
gcloud compute backend-services create bio-qms-docs-backend \
--global \
--enable-cdn \
--cache-mode=CACHE_ALL_STATIC \
--default-ttl=3600 \
--max-ttl=86400 \
--client-ttl=3600 \
--custom-response-header="Strict-Transport-Security: max-age=31536000; includeSubDomains; preload"
gcloud compute backend-services add-backend bio-qms-docs-backend \
--global \
--network-endpoint-group=bio-qms-docs-neg \
--network-endpoint-group-region=us-central1
Terraform:
resource "google_compute_backend_service" "bio_qms_docs" {
name = "bio-qms-docs-backend"
protocol = "HTTP"
port_name = "http"
timeout_sec = 30
enable_cdn = true
compression_mode = "AUTOMATIC"
cdn_policy {
cache_mode = "CACHE_ALL_STATIC"
default_ttl = 3600
max_ttl = 86400
client_ttl = 3600
negative_caching = true
cache_key_policy {
include_host = true
include_protocol = true
include_query_string = true
}
}
custom_response_headers = [
"Strict-Transport-Security: max-age=31536000; includeSubDomains; preload",
"X-Frame-Options: DENY",
"X-Content-Type-Options: nosniff",
"Referrer-Policy: strict-origin-when-cross-origin"
]
backend {
group = google_compute_region_network_endpoint_group.bio_qms_docs.id
}
log_config {
enable = true
sample_rate = 1.0
}
}
11.2.4 URL Map
gcloud compute url-maps create bio-qms-docs-urlmap \
--default-service=bio-qms-docs-backend
Terraform:
resource "google_compute_url_map" "bio_qms_docs" {
name = "bio-qms-docs-urlmap"
default_service = google_compute_backend_service.bio_qms_docs.id
host_rule {
hosts = ["bio-qms.docs.coditect.ai"]
path_matcher = "main"
}
path_matcher {
name = "main"
default_service = google_compute_backend_service.bio_qms_docs.id
}
}
11.2.5 HTTPS Proxy
gcloud compute target-https-proxies create bio-qms-docs-https-proxy \
--url-map=bio-qms-docs-urlmap \
--ssl-certificates=bio-qms-docs-cert \
--ssl-policy=bio-qms-tls-policy
Terraform:
resource "google_compute_target_https_proxy" "bio_qms_docs" {
name = "bio-qms-docs-https-proxy"
url_map = google_compute_url_map.bio_qms_docs.id
ssl_certificates = [google_compute_managed_ssl_certificate.bio_qms_docs.id]
ssl_policy = google_compute_ssl_policy.bio_qms_tls.id
}
11.2.6 HTTPS Forwarding Rule
gcloud compute forwarding-rules create bio-qms-docs-https-rule \
--global \
--target-https-proxy=bio-qms-docs-https-proxy \
--address=bio-qms-docs-ip \
--ports=443
Terraform:
resource "google_compute_global_forwarding_rule" "bio_qms_https" {
name = "bio-qms-docs-https-rule"
target = google_compute_target_https_proxy.bio_qms_docs.id
ip_address = google_compute_global_address.bio_qms_docs_ipv4.address
port_range = "443"
}
# IPv6 forwarding rule
resource "google_compute_global_forwarding_rule" "bio_qms_https_ipv6" {
name = "bio-qms-docs-https-rule-ipv6"
target = google_compute_target_https_proxy.bio_qms_docs.id
ip_address = google_compute_global_address.bio_qms_docs_ipv6.address
port_range = "443"
}
11.3 Complete Terraform Module
File: terraform/modules/docs-platform/main.tf
variable "project_name" {
description = "Project identifier (e.g., bio-qms)"
type = string
}
variable "cloud_run_service" {
description = "Cloud Run service name"
type = string
}
variable "cloud_run_region" {
description = "Cloud Run service region"
type = string
default = "us-central1"
}
variable "domain" {
description = "Custom domain (e.g., bio-qms.docs.coditect.ai)"
type = string
}
# Reserve global IP
resource "google_compute_global_address" "main" {
name = "${var.project_name}-docs-ip"
ip_version = "IPV4"
}
# Serverless NEG
resource "google_compute_region_network_endpoint_group" "main" {
name = "${var.project_name}-docs-neg"
region = var.cloud_run_region
network_endpoint_type = "SERVERLESS"
cloud_run {
service = var.cloud_run_service
}
}
# Backend service with Cloud CDN
resource "google_compute_backend_service" "main" {
name = "${var.project_name}-docs-backend"
protocol = "HTTP"
enable_cdn = true
compression_mode = "AUTOMATIC"
cdn_policy {
cache_mode = "CACHE_ALL_STATIC"
default_ttl = 3600
max_ttl = 86400
client_ttl = 3600
negative_caching = true
cache_key_policy {
include_host = true
include_protocol = true
include_query_string = true
}
}
custom_response_headers = [
"Strict-Transport-Security: max-age=31536000; includeSubDomains; preload",
"X-Frame-Options: DENY",
"X-Content-Type-Options: nosniff",
"Referrer-Policy: strict-origin-when-cross-origin",
"Permissions-Policy: geolocation=(), microphone=(), camera=()"
]
backend {
group = google_compute_region_network_endpoint_group.main.id
}
log_config {
enable = true
sample_rate = 1.0
}
}
# SSL certificate
resource "google_compute_managed_ssl_certificate" "main" {
name = "${var.project_name}-docs-cert"
managed {
domains = [var.domain]
}
}
# TLS policy
resource "google_compute_ssl_policy" "main" {
name = "${var.project_name}-tls-policy"
profile = "MODERN"
min_tls_version = "TLS_1_2"
}
# URL map
resource "google_compute_url_map" "main" {
name = "${var.project_name}-docs-urlmap"
default_service = google_compute_backend_service.main.id
host_rule {
hosts = [var.domain]
path_matcher = "main"
}
path_matcher {
name = "main"
default_service = google_compute_backend_service.main.id
}
}
# HTTPS proxy
resource "google_compute_target_https_proxy" "main" {
name = "${var.project_name}-docs-https-proxy"
url_map = google_compute_url_map.main.id
ssl_certificates = [google_compute_managed_ssl_certificate.main.id]
ssl_policy = google_compute_ssl_policy.main.id
}
# HTTPS forwarding rule
resource "google_compute_global_forwarding_rule" "https" {
name = "${var.project_name}-docs-https-rule"
target = google_compute_target_https_proxy.main.id
ip_address = google_compute_global_address.main.address
port_range = "443"
}
# HTTP redirect URL map
resource "google_compute_url_map" "http_redirect" {
name = "${var.project_name}-docs-http-redirect"
default_url_redirect {
https_redirect = true
redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
strip_query = false
}
}
# HTTP proxy
resource "google_compute_target_http_proxy" "main" {
name = "${var.project_name}-docs-http-proxy"
url_map = google_compute_url_map.http_redirect.id
}
# HTTP forwarding rule
resource "google_compute_global_forwarding_rule" "http" {
name = "${var.project_name}-docs-http-rule"
target = google_compute_target_http_proxy.main.id
ip_address = google_compute_global_address.main.address
port_range = "80"
}
# Outputs
output "ip_address" {
description = "Global IP address for DNS A record"
value = google_compute_global_address.main.address
}
output "certificate_status" {
description = "SSL certificate provisioning status"
value = google_compute_managed_ssl_certificate.main.managed[0].status
}
output "url" {
description = "Documentation site URL"
value = "https://${var.domain}"
}
Usage:
module "bio_qms_docs_platform" {
source = "./modules/docs-platform"
project_name = "bio-qms"
cloud_run_service = "bio-qms-docs"
cloud_run_region = "us-central1"
domain = "bio-qms.docs.coditect.ai"
}
output "bio_qms_ip" {
value = module.bio_qms_docs_platform.ip_address
}
12. Certificate Monitoring and Renewal
12.1 Google-Managed Auto-Renewal
Renewal Timeline:
- Certificate issued with 90-day validity
- Google attempts renewal 30 days before expiration
- Renewal uses same HTTP-01 challenge (transparent to user)
- New certificate deployed automatically with zero downtime
User Actions Required: NONE (fully automated)
12.2 Monitoring Certificate Expiry
Cloud Monitoring Alert Policy:
displayName: SSL Certificate Expiry Warning
conditions:
- displayName: Certificate expires in <30 days
conditionThreshold:
filter: |
resource.type = "loadbalancing_https_proxy"
metric.type = "loadbalancing.googleapis.com/https/certificate/time_to_expiration"
comparison: COMPARISON_LT
thresholdValue: 2592000 # 30 days in seconds
duration: 600s
notificationChannels:
- <EMAIL_CHANNEL_ID>
alertStrategy:
autoClose: 86400s
gcloud Command:
gcloud alpha monitoring policies create \
--notification-channels=<CHANNEL_ID> \
--display-name="SSL Certificate Expiry Warning" \
--condition-display-name="Certificate expires in <30 days" \
--condition-threshold-value=2592000 \
--condition-threshold-duration=600s \
--condition-filter='resource.type = "loadbalancing_https_proxy" AND metric.type = "loadbalancing.googleapis.com/https/certificate/time_to_expiration"' \
--condition-comparison=COMPARISON_LT
Terraform:
resource "google_monitoring_alert_policy" "cert_expiry" {
display_name = "SSL Certificate Expiry Warning"
combiner = "OR"
conditions {
display_name = "Certificate expires in <30 days"
condition_threshold {
filter = <<-EOT
resource.type = "loadbalancing_https_proxy"
metric.type = "loadbalancing.googleapis.com/https/certificate/time_to_expiration"
resource.label.target_proxy_name = "bio-qms-docs-https-proxy"
EOT
duration = "600s"
comparison = "COMPARISON_LT"
threshold_value = 2592000 # 30 days
}
}
notification_channels = [google_monitoring_notification_channel.email.id]
}
resource "google_monitoring_notification_channel" "email" {
display_name = "DevOps Team Email"
type = "email"
labels = {
email_address = "devops@coditect.ai"
}
}
12.3 Manual Certificate Inspection
OpenSSL Command:
echo | openssl s_client -connect bio-qms.docs.coditect.ai:443 -servername bio-qms.docs.coditect.ai 2>/dev/null | openssl x509 -noout -dates
Output:
notBefore=Jan 15 12:34:56 2026 GMT
notAfter=Apr 15 12:34:56 2026 GMT
Expiry Check Script:
#!/bin/bash
DOMAIN="bio-qms.docs.coditect.ai"
EXPIRY=$(echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt 30 ]; then
echo "WARNING: Certificate expires in $DAYS_LEFT days"
exit 1
else
echo "OK: Certificate expires in $DAYS_LEFT days"
exit 0
fi
12.4 Renewal Failure Handling
Symptoms:
- Certificate status shows
RENEWAL_FAILED - Browser shows "Your connection is not private" after expiry
Diagnosis:
gcloud compute ssl-certificates describe bio-qms-docs-cert \
--global \
--format="yaml(managed.status, managed.domainStatus)"
Common Causes:
| Cause | Resolution |
|---|---|
| DNS changed (A record no longer points to LB) | Update DNS A record to load balancer IP |
| Load balancer deleted | Recreate load balancer with same IP |
| CAA record blocks renewal | Update CAA record to allow pki.goog |
| Let's Encrypt rate limit | Wait 7 days or migrate to wildcard cert |
Manual Renewal Trigger:
# Delete and recreate certificate (forces new issuance)
gcloud compute ssl-certificates delete bio-qms-docs-cert --global --quiet
gcloud compute ssl-certificates create bio-qms-docs-cert \
--domains=bio-qms.docs.coditect.ai \
--global
# Re-attach to HTTPS proxy
gcloud compute target-https-proxies update bio-qms-docs-https-proxy \
--ssl-certificates=bio-qms-docs-cert
13. Downtime-Free Certificate Rotation
13.1 Dual-Certificate Attachment
GCP Load Balancer supports multiple SSL certificates on a single HTTPS proxy.
Use Case: Migrate from single-domain to wildcard certificate with zero downtime.
Process:
- Attach both certificates:
gcloud compute target-https-proxies update bio-qms-docs-https-proxy \
--ssl-certificates=bio-qms-docs-cert,docs-coditect-ai-wildcard
- Wait for new certificate to become ACTIVE:
watch gcloud compute ssl-certificates describe docs-coditect-ai-wildcard \
--global \
--format="value(managed.status)"
- Remove old certificate:
gcloud compute target-https-proxies update bio-qms-docs-https-proxy \
--ssl-certificates=docs-coditect-ai-wildcard
- Delete old certificate (after 30-day grace period):
gcloud compute ssl-certificates delete bio-qms-docs-cert --global
Downtime: 0 seconds (load balancer serves from either cert during overlap).
13.2 Certificate SNI Routing
Server Name Indication (SNI): Browser sends bio-qms.docs.coditect.ai in TLS handshake.
Load balancer selects certificate matching SNI:
bio-qms.docs.coditect.ai→ Matches bothbio-qms.docs.coditect.aiand*.docs.coditect.ai- Load balancer prefers exact match over wildcard
Edge Case: Browsers without SNI (IE 6 on Windows XP) receive default certificate (negligible traffic <0.01%).
14. CAA Records for Certificate Authority Restriction
14.1 CAA Record Purpose
Certification Authority Authorization (CAA): DNS record type that restricts which CAs can issue certificates for a domain.
Security Benefit: Prevents unauthorized certificate issuance by rogue CAs.
14.2 CAA Record Configuration
Record Format:
<domain> <ttl> IN CAA <flags> <tag> "<value>"
Tags:
issue: Authorize CA to issue certificates for this domainissuewild: Authorize CA to issue wildcard certificates for this domainiodef: Email/URL to report unauthorized issuance attempts
BIO-QMS CAA Records:
bio-qms.docs.coditect.ai. 3600 IN CAA 0 issue "pki.goog"
bio-qms.docs.coditect.ai. 3600 IN CAA 0 issuewild "pki.goog"
bio-qms.docs.coditect.ai. 3600 IN CAA 0 iodef "mailto:security@coditect.ai"
gcloud Command:
gcloud dns record-sets create bio-qms.docs.coditect.ai. \
--zone=docs-coditect-ai \
--type=CAA \
--ttl=3600 \
--rrdatas='0 issue "pki.goog"','0 issuewild "pki.goog"','0 iodef "mailto:security@coditect.ai"'
Terraform:
resource "google_dns_record_set" "caa" {
name = "bio-qms.docs.coditect.ai."
managed_zone = google_dns_managed_zone.docs_coditect_ai.name
type = "CAA"
ttl = 3600
rrdatas = [
"0 issue \"pki.goog\"",
"0 issuewild \"pki.goog\"",
"0 iodef \"mailto:security@coditect.ai\""
]
}
14.3 CAA Verification
Query CAA Records:
dig bio-qms.docs.coditect.ai CAA +short
Output:
0 issue "pki.goog"
0 issuewild "pki.goog"
0 iodef "mailto:security@coditect.ai"
CAA Lookup Tool: https://caatest.co.uk/
15. DNSSEC Configuration
15.1 DNSSEC Purpose
Domain Name System Security Extensions (DNSSEC): Cryptographic signatures on DNS records to prevent DNS spoofing and cache poisoning.
Security Benefit:
- Authenticates DNS responses (proves they came from authoritative server)
- Detects tampering (modified DNS records break signature)
- Prevents man-in-the-middle attacks on DNS
15.2 Enable DNSSEC on Cloud DNS
Managed Zone Creation with DNSSEC:
gcloud dns managed-zones create docs-coditect-ai \
--dns-name="docs.coditect.ai." \
--description="CODITECT Documentation Platform" \
--dnssec-state=on
Enable DNSSEC on Existing Zone:
gcloud dns managed-zones update docs-coditect-ai \
--dnssec-state=on
Terraform:
resource "google_dns_managed_zone" "docs_coditect_ai" {
name = "docs-coditect-ai"
dns_name = "docs.coditect.ai."
description = "CODITECT Documentation Platform"
dnssec_config {
state = "on"
non_existence = "nsec3"
default_key_specs {
algorithm = "rsasha256"
key_length = 2048
key_type = "keySigning"
}
default_key_specs {
algorithm = "rsasha256"
key_length = 1024
key_type = "zoneSigning"
}
}
}
15.3 DS Record Delegation
Purpose: Parent zone (coditect.ai) must publish DS (Delegation Signer) record pointing to child zone's DNSKEY.
Retrieve DS Records:
gcloud dns dns-keys describe <KEY_ID> \
--zone=docs-coditect-ai \
--format="value(ds_record())"
Example DS Record:
docs.coditect.ai. IN DS 12345 8 2 A1B2C3D4E5F6...
Manual Step: Add DS record at parent zone registrar (e.g., Google Domains).
Verification:
dig docs.coditect.ai DS +dnssec
15.4 DNSSEC Validation Testing
Test DNSSEC Validation:
dig bio-qms.docs.coditect.ai A +dnssec
Look for ad (Authenticated Data) flag in response:
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
Online Validator: https://dnsviz.net/d/bio-qms.docs.coditect.ai/dnssec/
16. Disaster Recovery
16.1 Domain Transfer Preparation
Scenario: Transfer docs.coditect.ai to new registrar or DNS provider.
Prerequisites:
- Unlock domain at current registrar
- Obtain authorization code (EPP code)
- Export DNS zone file
- Document load balancer IP addresses
DNS Zone Export:
gcloud dns record-sets export zone.yaml \
--zone=docs-coditect-ai \
--zone-file-format
Load Balancer IP Backup:
gcloud compute addresses list --global --format="table(name, address, addressType)"
16.2 Certificate Backup
Google-Managed Certificates Cannot Be Exported (Private Key Stays in Google HSM).
Recovery Strategy:
- Create new Google-managed certificate in target GCP project
- Point DNS to new load balancer
- Wait 30-60 minutes for certificate provisioning
- Old certificate remains valid during transition (dual-cert attachment)
Alternative: Self-Managed Certificates
# Generate private key
openssl genrsa -out bio-qms.key 2048
# Generate CSR
openssl req -new -key bio-qms.key -out bio-qms.csr \
-subj "/CN=bio-qms.docs.coditect.ai"
# Submit CSR to CA (e.g., Let's Encrypt, DigiCert)
# Receive certificate + intermediate chain
# Upload to GCP
gcloud compute ssl-certificates create bio-qms-docs-cert-manual \
--certificate=bio-qms.crt \
--private-key=bio-qms.key \
--global
Security Consideration: Self-managed certificates require manual renewal and private key protection.
16.3 Load Balancer Configuration Backup
Export URL Map:
gcloud compute url-maps describe bio-qms-docs-urlmap \
--global \
--format=yaml > urlmap-backup.yaml
Export Backend Service:
gcloud compute backend-services describe bio-qms-docs-backend \
--global \
--format=yaml > backend-backup.yaml
Terraform State Backup:
terraform state pull > terraform-state-backup.json
16.4 RTO/RPO Targets
| Scenario | RTO (Recovery Time) | RPO (Data Loss) |
|---|---|---|
| DNS Provider Failure | 5 minutes | 0 (DNS is cached) |
| GCP Load Balancer Outage | 5 minutes (multi-region) | 0 (stateless) |
| Certificate Expiry | 30-60 minutes (new cert provisioning) | 0 (no data) |
| Complete GCP Project Loss | 2-4 hours (rebuild infra from Terraform) | 0 (content in Cloud Run/Storage) |
17. Compliance Considerations
17.1 21 CFR Part 11 Requirements
Regulation: FDA 21 CFR Part 11 - Electronic Records and Signatures
Relevant Provisions:
- §11.10(a): Validation of systems to ensure accuracy, reliability, consistent intended performance
- §11.10(d): Limiting system access to authorized individuals
- §11.10(e): Use of secure, computer-generated, time-stamped audit trails
- §11.30: Controls for open systems (systems accessible via public networks)
17.2 Data Transmission Security (§11.30)
Requirement: Persons who use open systems to create, modify, maintain, or transmit electronic records shall employ procedures and controls designed to ensure the authenticity, integrity, and confidentiality of electronic records.
Compliance Implementation:
| Control | Implementation |
|---|---|
| Authenticity | TLS certificates verify server identity via PKI |
| Integrity | TLS AEAD ciphers prevent message tampering |
| Confidentiality | TLS 1.3 encryption protects data in transit |
| Non-repudiation | Load balancer access logs (timestamp, IP, user agent) |
| Audit Trail | Cloud Logging captures all HTTPS requests |
17.3 Audit Trail Requirements
Log Data Captured:
- Timestamp (UTC, millisecond precision)
- Source IP address
- TLS version and cipher suite
- Request path and query string
- HTTP status code
- User agent string
Log Retention: 7 years (per 21 CFR Part 11 guidance)
Log Immutability: Cloud Logging retention locked (prevents deletion/modification)
gcloud Command (Enable Access Logging):
gcloud compute backend-services update bio-qms-docs-backend \
--global \
--enable-logging \
--logging-sample-rate=1.0
Terraform:
resource "google_compute_backend_service" "bio_qms_docs" {
# ... other config ...
log_config {
enable = true
sample_rate = 1.0 # Log 100% of requests
}
}
17.4 Access Control (§11.10(d))
Requirement: Limiting system access to authorized individuals.
Implementation:
- Load balancer publicly accessible (documentation is public)
- Cloud Run service configured with IAM authentication (backend API)
- TLS client certificates for service-to-service communication (if required)
Optional: mTLS (Mutual TLS) for Admin Access:
resource "google_compute_backend_service" "bio_qms_docs" {
# ... other config ...
iap {
oauth2_client_id = google_iap_client.bio_qms.client_id
oauth2_client_secret = google_iap_client.bio_qms.secret
}
}
17.5 System Validation (§11.10(a))
Validation Activities:
- Installation Qualification (IQ): Verify GCP load balancer deployed per spec
- Operational Qualification (OQ): Test TLS handshake, certificate validation, redirect behavior
- Performance Qualification (PQ): Load testing, latency measurement, failover testing
Validation Documentation:
- Infrastructure-as-Code (Terraform) serves as design specification
- Automated tests verify configuration (see Section 22)
- Change control via Git commits (audit trail)
18. Performance Considerations
18.1 TLS Handshake Optimization
TLS 1.3 vs TLS 1.2 Handshake:
| Protocol | Round Trips | Latency (150ms RTT) |
|---|---|---|
| TLS 1.2 | 2-RTT | 300ms |
| TLS 1.3 | 1-RTT | 150ms |
| TLS 1.3 0-RTT | 0-RTT (resumption) | 0ms |
0-RTT Resumption (Session Resumption):
- Client stores session ticket from previous connection
- Sends encrypted application data in first packet (0-RTT)
- Reduces latency by 100-300ms on repeat visits
GCP Load Balancer: Automatically enables TLS 1.3 0-RTT for resumed sessions.
18.2 OCSP Stapling
Online Certificate Status Protocol (OCSP): Browser checks certificate revocation status.
Without Stapling:
- Browser makes TLS connection to
bio-qms.docs.coditect.ai - Browser makes OCSP request to CA (e.g.,
ocsp.pki.goog) - CA responds with certificate status
- Latency: +100-500ms (extra DNS lookup + OCSP request)
With Stapling:
- Load balancer fetches OCSP response from CA every 24 hours
- Load balancer sends OCSP response in TLS handshake
- Latency: 0ms (no additional request)
GCP Load Balancer: OCSP stapling enabled by default.
Verification:
echo | openssl s_client -connect bio-qms.docs.coditect.ai:443 -status 2>/dev/null | grep "OCSP Response"
Output:
OCSP Response Status: successful (0x0)
18.3 HTTP/2 and HTTP/3
HTTP/2 Benefits:
- Multiplexing (multiple requests over single TLS connection)
- Header compression (HPACK)
- Server push (proactively send resources)
HTTP/3 Benefits (QUIC):
- 0-RTT connection establishment (TLS 1.3 + QUIC handshake combined)
- Improved congestion control
- Connection migration (survives IP address changes)
GCP Load Balancer Support:
- HTTP/2: Enabled by default
- HTTP/3: In beta (not yet production-ready for all regions)
Enable HTTP/3 (when available):
gcloud compute target-https-proxies update bio-qms-docs-https-proxy \
--quic-override=ENABLE
18.4 CDN Cache Hit Ratio
Cloud CDN Caching Strategy:
- Static assets (HTML, CSS, JS, images): Cached for 1 hour (default TTL)
- Cache key: Host + Path + Query String
- Invalidation: Manual via
gcloud compute url-maps invalidate-cdn-cache
Target Metrics:
- Cache Hit Ratio: >90%
- Origin Requests: <10% of total traffic
- Median Latency: <50ms (edge serving)
Monitoring:
gcloud monitoring timeseries list \
--filter='metric.type="loadbalancing.googleapis.com/https/request_count" AND metric.labels.cache_result!="MISS"' \
--format="table(metric.labels.cache_result, value)"
19. Testing and Validation Procedures
19.1 DNS Propagation Testing
DNS Resolution Test:
# Query authoritative nameservers
dig @ns-cloud-a1.googledomains.com bio-qms.docs.coditect.ai A +short
# Query public resolver
dig @8.8.8.8 bio-qms.docs.coditect.ai A +short
# Query local resolver
dig bio-qms.docs.coditect.ai A +short
Expected Output:
<LOAD_BALANCER_IP>
Global Propagation Check: https://www.whatsmydns.net/#A/bio-qms.docs.coditect.ai
19.2 TLS Certificate Validation
OpenSSL Certificate Check:
echo | openssl s_client -connect bio-qms.docs.coditect.ai:443 -servername bio-qms.docs.coditect.ai 2>/dev/null | openssl x509 -noout -text
Validation Checklist:
- Subject CN matches
bio-qms.docs.coditect.ai - Issuer is
Google Trust Services - Not Before date is in past
- Not After date is >30 days in future
- Subject Alternative Names include
bio-qms.docs.coditect.ai - Signature algorithm is
sha256WithRSAEncryption
SSL Labs Test: https://www.ssllabs.com/ssltest/analyze.html?d=bio-qms.docs.coditect.ai
Target Grade: A+ (requires HSTS preload)
19.3 HTTP to HTTPS Redirect Test
HTTP Request:
curl -I http://bio-qms.docs.coditect.ai/test-path?query=value
Expected Response:
HTTP/1.1 301 Moved Permanently
Location: https://bio-qms.docs.coditect.ai/test-path?query=value
Validation:
- Status code is
301 Moved Permanently -
Locationheader useshttps://scheme - Path and query string preserved
19.4 Security Headers Test
Headers Check:
curl -I https://bio-qms.docs.coditect.ai
Expected Headers:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
Automated Test (Python):
import requests
url = "https://bio-qms.docs.coditect.ai"
response = requests.get(url)
expected_headers = {
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin"
}
for header, expected_value in expected_headers.items():
actual_value = response.headers.get(header)
assert actual_value == expected_value, f"{header}: expected {expected_value}, got {actual_value}"
print("✓ All security headers present")
19.5 Load Balancer Health Check
Backend Health Status:
gcloud compute backend-services get-health bio-qms-docs-backend --global
Expected Output:
backend: <BACKEND_URL>
status:
healthStatus:
- healthState: HEALTHY
instance: <NEG_ENDPOINT>
kind: compute#backendServiceGroupHealth
19.6 End-to-End Connectivity Test
Full Request Flow:
curl -v https://bio-qms.docs.coditect.ai/ -w "\n\nTime Total: %{time_total}s\nTime DNS: %{time_namelookup}s\nTime Connect: %{time_connect}s\nTime TLS: %{time_appconnect}s\nTime First Byte: %{time_starttransfer}s\n"
Expected Metrics:
- DNS lookup: <50ms
- TCP connect: <100ms
- TLS handshake: <200ms (TLS 1.3 1-RTT)
- Time to first byte: <500ms
- Total time: <1000ms
19.7 CAA Record Validation
CAA Check:
dig bio-qms.docs.coditect.ai CAA +short
Expected Output:
0 issue "pki.goog"
0 issuewild "pki.goog"
CAA Test Tool: https://caatest.co.uk/bio-qms.docs.coditect.ai
19.8 DNSSEC Validation
DNSSEC Check:
dig bio-qms.docs.coditect.ai A +dnssec | grep "ad;"
Expected: ad flag present (Authenticated Data)
DNSViz Test: https://dnsviz.net/d/bio-qms.docs.coditect.ai/dnssec/
20. Operational Procedures
20.1 Certificate Status Monitoring
Daily Monitoring Routine:
#!/bin/bash
# File: check-cert-status.sh
CERT_NAME="bio-qms-docs-cert"
MIN_DAYS=30
STATUS=$(gcloud compute ssl-certificates describe $CERT_NAME --global --format="value(managed.status)")
if [ "$STATUS" != "ACTIVE" ]; then
echo "❌ Certificate status: $STATUS (expected ACTIVE)"
exit 1
fi
EXPIRY=$(echo | openssl s_client -connect bio-qms.docs.coditect.ai:443 -servername bio-qms.docs.coditect.ai 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt $MIN_DAYS ]; then
echo "⚠️ Certificate expires in $DAYS_LEFT days (threshold: $MIN_DAYS days)"
exit 1
else
echo "✅ Certificate expires in $DAYS_LEFT days"
exit 0
fi
Cron Job:
0 9 * * * /opt/scripts/check-cert-status.sh || mail -s "SSL Cert Alert" devops@coditect.ai
20.2 Load Balancer Traffic Monitoring
Request Rate:
gcloud monitoring timeseries list \
--filter='metric.type="loadbalancing.googleapis.com/https/request_count" AND resource.labels.url_map_name="bio-qms-docs-urlmap"' \
--format="table(point.interval.end_time, point.value)"
Latency (95th Percentile):
gcloud monitoring timeseries list \
--filter='metric.type="loadbalancing.googleapis.com/https/total_latencies" AND resource.labels.url_map_name="bio-qms-docs-urlmap"' \
--format="table(point.interval.end_time, point.value)"
Error Rate (5xx Responses):
gcloud monitoring timeseries list \
--filter='metric.type="loadbalancing.googleapis.com/https/request_count" AND metric.labels.response_code_class="500"' \
--format="table(point.interval.end_time, point.value)"
20.3 DNS Zone Update Procedure
Add New Record:
# Dry run (preview changes)
gcloud dns record-sets transaction start --zone=docs-coditect-ai
gcloud dns record-sets transaction add <IP> \
--name=new-subdomain.docs.coditect.ai. \
--ttl=300 \
--type=A \
--zone=docs-coditect-ai
gcloud dns record-sets transaction describe --zone=docs-coditect-ai
# Execute
gcloud dns record-sets transaction execute --zone=docs-coditect-ai
Update Existing Record:
# Remove old record
gcloud dns record-sets transaction start --zone=docs-coditect-ai
gcloud dns record-sets transaction remove <OLD_IP> \
--name=bio-qms.docs.coditect.ai. \
--ttl=300 \
--type=A \
--zone=docs-coditect-ai
# Add new record
gcloud dns record-sets transaction add <NEW_IP> \
--name=bio-qms.docs.coditect.ai. \
--ttl=300 \
--type=A \
--zone=docs-coditect-ai
# Execute
gcloud dns record-sets transaction execute --zone=docs-coditect-ai
20.4 Emergency Rollback
Scenario: New certificate or load balancer configuration causes outage.
Rollback Procedure:
- Identify last known good Terraform state:
terraform state pull > current-state.json
git checkout <LAST_GOOD_COMMIT>
- Apply previous configuration:
terraform plan # Review changes
terraform apply -auto-approve
- Verify rollback:
curl -I https://bio-qms.docs.coditect.ai
gcloud compute ssl-certificates describe bio-qms-docs-cert --global
- Restore DNS if needed:
gcloud dns record-sets transaction start --zone=docs-coditect-ai
gcloud dns record-sets transaction remove <BAD_IP> --name=bio-qms.docs.coditect.ai. --ttl=300 --type=A --zone=docs-coditect-ai
gcloud dns record-sets transaction add <GOOD_IP> --name=bio-qms.docs.coditect.ai. --ttl=300 --type=A --zone=docs-coditect-ai
gcloud dns record-sets transaction execute --zone=docs-coditect-ai
21. Appendix
21.1 Complete Terraform Configuration
File: terraform/bio-qms-docs-platform.tf
terraform {
required_version = ">= 1.0"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
backend "gcs" {
bucket = "coditect-terraform-state"
prefix = "bio-qms/docs-platform"
}
}
provider "google" {
project = var.project_id
region = var.region
}
variable "project_id" {
description = "GCP project ID"
type = string
}
variable "region" {
description = "Default GCP region"
type = string
default = "us-central1"
}
variable "domain" {
description = "Custom domain for documentation site"
type = string
default = "bio-qms.docs.coditect.ai"
}
variable "cloud_run_service" {
description = "Cloud Run service name"
type = string
default = "bio-qms-docs"
}
# DNS Managed Zone
resource "google_dns_managed_zone" "docs_coditect_ai" {
name = "docs-coditect-ai"
dns_name = "docs.coditect.ai."
description = "CODITECT Documentation Platform - Multi-Project Zone"
visibility = "public"
dnssec_config {
state = "on"
non_existence = "nsec3"
default_key_specs {
algorithm = "rsasha256"
key_length = 2048
key_type = "keySigning"
}
default_key_specs {
algorithm = "rsasha256"
key_length = 1024
key_type = "zoneSigning"
}
}
}
# Global IP Address
resource "google_compute_global_address" "bio_qms_docs_ipv4" {
name = "bio-qms-docs-ip"
ip_version = "IPV4"
}
resource "google_compute_global_address" "bio_qms_docs_ipv6" {
name = "bio-qms-docs-ip-ipv6"
ip_version = "IPV6"
}
# DNS A Record
resource "google_dns_record_set" "bio_qms_a" {
name = "${var.domain}."
managed_zone = google_dns_managed_zone.docs_coditect_ai.name
type = "A"
ttl = 300
rrdatas = [google_compute_global_address.bio_qms_docs_ipv4.address]
}
# DNS AAAA Record (IPv6)
resource "google_dns_record_set" "bio_qms_aaaa" {
name = "${var.domain}."
managed_zone = google_dns_managed_zone.docs_coditect_ai.name
type = "AAAA"
ttl = 300
rrdatas = [google_compute_global_address.bio_qms_docs_ipv6.address]
}
# CAA Records
resource "google_dns_record_set" "bio_qms_caa" {
name = "${var.domain}."
managed_zone = google_dns_managed_zone.docs_coditect_ai.name
type = "CAA"
ttl = 3600
rrdatas = [
"0 issue \"pki.goog\"",
"0 issuewild \"pki.goog\"",
"0 iodef \"mailto:security@coditect.ai\""
]
}
# Serverless NEG
resource "google_compute_region_network_endpoint_group" "bio_qms_docs" {
name = "bio-qms-docs-neg"
region = var.region
network_endpoint_type = "SERVERLESS"
cloud_run {
service = var.cloud_run_service
}
}
# Backend Service with Cloud CDN
resource "google_compute_backend_service" "bio_qms_docs" {
name = "bio-qms-docs-backend"
protocol = "HTTP"
port_name = "http"
timeout_sec = 30
enable_cdn = true
compression_mode = "AUTOMATIC"
cdn_policy {
cache_mode = "CACHE_ALL_STATIC"
default_ttl = 3600
max_ttl = 86400
client_ttl = 3600
negative_caching = true
cache_key_policy {
include_host = true
include_protocol = true
include_query_string = true
}
}
custom_response_headers = [
"Strict-Transport-Security: max-age=31536000; includeSubDomains; preload",
"X-Frame-Options: DENY",
"X-Content-Type-Options: nosniff",
"Referrer-Policy: strict-origin-when-cross-origin",
"Permissions-Policy: geolocation=(), microphone=(), camera=()"
]
backend {
group = google_compute_region_network_endpoint_group.bio_qms_docs.id
}
log_config {
enable = true
sample_rate = 1.0
}
}
# SSL Certificate
resource "google_compute_managed_ssl_certificate" "bio_qms_docs" {
name = "bio-qms-docs-cert"
managed {
domains = [var.domain]
}
}
# TLS Policy
resource "google_compute_ssl_policy" "bio_qms_tls" {
name = "bio-qms-tls-policy"
profile = "MODERN"
min_tls_version = "TLS_1_2"
}
# URL Map (HTTPS)
resource "google_compute_url_map" "bio_qms_docs" {
name = "bio-qms-docs-urlmap"
default_service = google_compute_backend_service.bio_qms_docs.id
host_rule {
hosts = [var.domain]
path_matcher = "main"
}
path_matcher {
name = "main"
default_service = google_compute_backend_service.bio_qms_docs.id
}
}
# HTTPS Proxy
resource "google_compute_target_https_proxy" "bio_qms_docs" {
name = "bio-qms-docs-https-proxy"
url_map = google_compute_url_map.bio_qms_docs.id
ssl_certificates = [google_compute_managed_ssl_certificate.bio_qms_docs.id]
ssl_policy = google_compute_ssl_policy.bio_qms_tls.id
}
# HTTPS Forwarding Rule (IPv4)
resource "google_compute_global_forwarding_rule" "bio_qms_https_ipv4" {
name = "bio-qms-docs-https-rule"
target = google_compute_target_https_proxy.bio_qms_docs.id
ip_address = google_compute_global_address.bio_qms_docs_ipv4.address
port_range = "443"
}
# HTTPS Forwarding Rule (IPv6)
resource "google_compute_global_forwarding_rule" "bio_qms_https_ipv6" {
name = "bio-qms-docs-https-rule-ipv6"
target = google_compute_target_https_proxy.bio_qms_docs.id
ip_address = google_compute_global_address.bio_qms_docs_ipv6.address
port_range = "443"
}
# URL Map (HTTP Redirect)
resource "google_compute_url_map" "bio_qms_http_redirect" {
name = "bio-qms-docs-http-redirect"
default_url_redirect {
https_redirect = true
redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
strip_query = false
}
}
# HTTP Proxy
resource "google_compute_target_http_proxy" "bio_qms_docs" {
name = "bio-qms-docs-http-proxy"
url_map = google_compute_url_map.bio_qms_http_redirect.id
}
# HTTP Forwarding Rule (IPv4)
resource "google_compute_global_forwarding_rule" "bio_qms_http_ipv4" {
name = "bio-qms-docs-http-rule"
target = google_compute_target_http_proxy.bio_qms_docs.id
ip_address = google_compute_global_address.bio_qms_docs_ipv4.address
port_range = "80"
}
# HTTP Forwarding Rule (IPv6)
resource "google_compute_global_forwarding_rule" "bio_qms_http_ipv6" {
name = "bio-qms-docs-http-rule-ipv6"
target = google_compute_target_http_proxy.bio_qms_docs.id
ip_address = google_compute_global_address.bio_qms_docs_ipv6.address
port_range = "80"
}
# Monitoring Alert: Certificate Expiry
resource "google_monitoring_notification_channel" "email" {
display_name = "DevOps Team Email"
type = "email"
labels = {
email_address = "devops@coditect.ai"
}
}
resource "google_monitoring_alert_policy" "cert_expiry" {
display_name = "SSL Certificate Expiry Warning"
combiner = "OR"
conditions {
display_name = "Certificate expires in <30 days"
condition_threshold {
filter = <<-EOT
resource.type = "loadbalancing_https_proxy"
metric.type = "loadbalancing.googleapis.com/https/certificate/time_to_expiration"
resource.label.target_proxy_name = "${google_compute_target_https_proxy.bio_qms_docs.name}"
EOT
duration = "600s"
comparison = "COMPARISON_LT"
threshold_value = 2592000 # 30 days in seconds
}
}
notification_channels = [google_monitoring_notification_channel.email.id]
alert_strategy {
auto_close = "86400s"
}
}
# Outputs
output "ipv4_address" {
description = "Global IPv4 address for DNS A record"
value = google_compute_global_address.bio_qms_docs_ipv4.address
}
output "ipv6_address" {
description = "Global IPv6 address for DNS AAAA record"
value = google_compute_global_address.bio_qms_docs_ipv6.address
}
output "certificate_status" {
description = "SSL certificate provisioning status"
value = google_compute_managed_ssl_certificate.bio_qms_docs.managed[0].status
}
output "url" {
description = "Documentation site URL"
value = "https://${var.domain}"
}
output "nameservers" {
description = "Cloud DNS nameservers (add to parent zone registrar)"
value = google_dns_managed_zone.docs_coditect_ai.name_servers
}
21.2 Deployment Checklist
Pre-Deployment:
- Terraform state backend configured (
gcsbucket) - Cloud Run service deployed (
bio-qms-docs) - IAM permissions granted (Compute Admin, DNS Admin)
- Parent domain registrar access (to add NS records)
Deployment:
terraform init
terraform plan -out=tfplan
terraform apply tfplan
Post-Deployment:
- Retrieve nameservers:
terraform output nameservers - Add NS records at parent zone registrar (
coditect.ai) - Wait for DNS propagation (5-60 minutes)
- Verify DNS resolution:
dig bio-qms.docs.coditect.ai A +short - Wait for certificate provisioning (30-60 minutes)
- Verify certificate status:
terraform output certificate_status(should beACTIVE) - Test HTTPS:
curl -I https://bio-qms.docs.coditect.ai - Test HTTP redirect:
curl -I http://bio-qms.docs.coditect.ai - Verify security headers:
curl -I https://bio-qms.docs.coditect.ai | grep -i strict-transport-security - Run SSL Labs test: https://www.ssllabs.com/ssltest/
21.3 Troubleshooting Guide
| Issue | Symptoms | Resolution |
|---|---|---|
| DNS not resolving | dig returns no results | Verify NS records at registrar, wait for propagation |
| Certificate stuck in PROVISIONING | Status not changing to ACTIVE after 2 hours | Check DNS points to LB IP, verify HTTP access, review CAA records |
| 502 Bad Gateway | Load balancer responds but backend unreachable | Check Cloud Run service health, verify NEG backend attached |
| SSL handshake failure | curl returns SSL error | Verify certificate attached to HTTPS proxy, check domain matches |
| HTTP not redirecting | HTTP request returns content instead of redirect | Verify HTTP forwarding rule points to redirect URL map |
| Missing security headers | Headers not present in response | Verify custom_response_headers in backend service config |
Document Control
Version History:
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0.0 | 2026-02-16 | Claude (Sonnet 4.5) | Initial technical specification |
Review Schedule: Quarterly (every 3 months)
Next Review: 2026-05-16
Document Owner: Infrastructure Engineering Team
Approval:
- Infrastructure Lead
- Security Lead
- Compliance Officer
End of Document