Skip to main content

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

ComponentResponsibility
Cloud DNSAuthoritative DNS zone for docs.coditect.ai, A/AAAA records pointing to load balancer IP
Load BalancerHTTPS frontend, certificate attachment, HTTP→HTTPS redirect, URL mapping
SSL CertificateGoogle-managed certificate for bio-qms.docs.coditect.ai, auto-renewal every 90 days
Cloud CDNEdge caching, TLS handshake optimization, OCSP stapling
Cloud RunBackend service, receives HTTP traffic from load balancer internal IP
Security HeadersHSTS, 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.ai for 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

PhaseDurationDescription
1. Certificate CreationInstantCreate managed certificate resource in GCP
2. Load Balancer AttachmentInstantAttach certificate to HTTPS frontend
3. DNS Propagation5-60 minA/AAAA records propagate globally
4. Domain Verification5-30 minGoogle verifies DNS ownership via HTTP-01 challenge
5. Certificate Issuance10-60 minGoogle Trust Services issues certificate
6. Certificate ActivationInstantCertificate becomes active on load balancer
Total20-150 minTypical: 30-60 minutes

5.2 Domain Verification Methods

Google-Managed Certificates use HTTP-01 challenge:

  1. User creates certificate with domain bio-qms.docs.coditect.ai
  2. User attaches certificate to load balancer HTTPS frontend
  3. User points DNS A record to load balancer IP
  4. Google sends HTTP request to http://bio-qms.docs.coditect.ai/.well-known/acme-challenge/<token>
  5. Load balancer responds with verification token
  6. 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 progress
  • ACTIVE: Certificate issued and deployed
  • RENEWAL_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 progress
  • ACTIVE: Domain verified and certificate issued
  • FAILED_NOT_VISIBLE: DNS not propagated or load balancer not accessible
  • FAILED_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 of bio-qms.docs.coditect.ai
  • preload: 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:

  1. HSTS header served on all requests (including HTTP redirects)
  2. max-age ≥ 31536000 (1 year)
  3. includeSubDomains directive present
  4. preload directive present
  5. Redirect HTTP to HTTPS on same host
  6. Serve HSTS header on base domain (not just subdomain)

Submission Process:

  1. Verify prerequisites at hstspreload.org
  2. Submit domain docs.coditect.ai (base domain, not subdomain)
  3. Wait 2-3 months for inclusion in Chromium preload list
  4. 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:

DirectiveValuePurpose
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 only
  • ALLOW-FROM https://example.com: Allow specific origin (deprecated, use CSP frame-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 if Content-Type missing
  • 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-ahttps://bio-qms.docs.coditect.ai/page-b)
  • Cross-origin HTTPS→HTTPS: Send origin only (https://bio-qms.docs.coditect.aihttps://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 requests
  • strict-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 only
  • https://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

ProjectDomainCertificate
BIO-QMSbio-qms.docs.coditect.aiCovered by *.docs.coditect.ai
Pharma MESpharma-mes.docs.coditect.aiCovered by *.docs.coditect.ai
Lab LIMSlab-lims.docs.coditect.aiCovered by *.docs.coditect.ai
Platform Docsplatform.docs.coditect.aiCovered 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:

  1. Create Wildcard Certificate:
gcloud compute ssl-certificates create docs-coditect-ai-wildcard \
--domains="*.docs.coditect.ai,docs.coditect.ai" \
--global
  1. 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
  1. 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:

CauseResolution
DNS changed (A record no longer points to LB)Update DNS A record to load balancer IP
Load balancer deletedRecreate load balancer with same IP
CAA record blocks renewalUpdate CAA record to allow pki.goog
Let's Encrypt rate limitWait 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:

  1. Attach both certificates:
gcloud compute target-https-proxies update bio-qms-docs-https-proxy \
--ssl-certificates=bio-qms-docs-cert,docs-coditect-ai-wildcard
  1. Wait for new certificate to become ACTIVE:
watch gcloud compute ssl-certificates describe docs-coditect-ai-wildcard \
--global \
--format="value(managed.status)"
  1. Remove old certificate:
gcloud compute target-https-proxies update bio-qms-docs-https-proxy \
--ssl-certificates=docs-coditect-ai-wildcard
  1. 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 both bio-qms.docs.coditect.ai and *.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 domain
  • issuewild: Authorize CA to issue wildcard certificates for this domain
  • iodef: 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:

  1. Unlock domain at current registrar
  2. Obtain authorization code (EPP code)
  3. Export DNS zone file
  4. 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:

  1. Create new Google-managed certificate in target GCP project
  2. Point DNS to new load balancer
  3. Wait 30-60 minutes for certificate provisioning
  4. 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

ScenarioRTO (Recovery Time)RPO (Data Loss)
DNS Provider Failure5 minutes0 (DNS is cached)
GCP Load Balancer Outage5 minutes (multi-region)0 (stateless)
Certificate Expiry30-60 minutes (new cert provisioning)0 (no data)
Complete GCP Project Loss2-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:

ControlImplementation
AuthenticityTLS certificates verify server identity via PKI
IntegrityTLS AEAD ciphers prevent message tampering
ConfidentialityTLS 1.3 encryption protects data in transit
Non-repudiationLoad balancer access logs (timestamp, IP, user agent)
Audit TrailCloud 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:

  1. Installation Qualification (IQ): Verify GCP load balancer deployed per spec
  2. Operational Qualification (OQ): Test TLS handshake, certificate validation, redirect behavior
  3. 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:

ProtocolRound TripsLatency (150ms RTT)
TLS 1.22-RTT300ms
TLS 1.31-RTT150ms
TLS 1.3 0-RTT0-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:

  1. Browser makes TLS connection to bio-qms.docs.coditect.ai
  2. Browser makes OCSP request to CA (e.g., ocsp.pki.goog)
  3. CA responds with certificate status
  4. Latency: +100-500ms (extra DNS lookup + OCSP request)

With Stapling:

  1. Load balancer fetches OCSP response from CA every 24 hours
  2. Load balancer sends OCSP response in TLS handshake
  3. 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
  • Location header uses https:// 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:

  1. Identify last known good Terraform state:
terraform state pull > current-state.json
git checkout <LAST_GOOD_COMMIT>
  1. Apply previous configuration:
terraform plan  # Review changes
terraform apply -auto-approve
  1. Verify rollback:
curl -I https://bio-qms.docs.coditect.ai
gcloud compute ssl-certificates describe bio-qms-docs-cert --global
  1. 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 (gcs bucket)
  • 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 be ACTIVE)
  • 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

IssueSymptomsResolution
DNS not resolvingdig returns no resultsVerify NS records at registrar, wait for propagation
Certificate stuck in PROVISIONINGStatus not changing to ACTIVE after 2 hoursCheck DNS points to LB IP, verify HTTP access, review CAA records
502 Bad GatewayLoad balancer responds but backend unreachableCheck Cloud Run service health, verify NEG backend attached
SSL handshake failurecurl returns SSL errorVerify certificate attached to HTTPS proxy, check domain matches
HTTP not redirectingHTTP request returns content instead of redirectVerify HTTP forwarding rule points to redirect URL map
Missing security headersHeaders not present in responseVerify custom_response_headers in backend service config

Document Control

Version History:

VersionDateAuthorChanges
1.0.02026-02-16Claude (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