Initial commit: Cloudflare infrastructure with WAF Intelligence
- Complete Cloudflare Terraform configuration (DNS, WAF, tunnels, access) - WAF Intelligence MCP server with threat analysis and ML classification - GitOps automation with PR workflows and drift detection - Observatory monitoring stack with Prometheus/Grafana - IDE operator rules for governed development - Security playbooks and compliance frameworks - Autonomous remediation and state reconciliation
This commit is contained in:
355
terraform/.gitlab-ci.yml
Normal file
355
terraform/.gitlab-ci.yml
Normal file
@@ -0,0 +1,355 @@
|
||||
stages:
|
||||
- validate
|
||||
- plan
|
||||
- gitops
|
||||
- approve
|
||||
- apply
|
||||
- compliance
|
||||
- reconcile
|
||||
|
||||
variables:
|
||||
TF_ROOT: ${CI_PROJECT_DIR}
|
||||
TF_STATE_NAME: cloudflare-infra
|
||||
TF_PLAN_FILE: tfplan.binary
|
||||
TF_PLAN_JSON: tfplan.json
|
||||
|
||||
cache:
|
||||
key: ${CI_COMMIT_REF_SLUG}
|
||||
paths:
|
||||
- ${TF_ROOT}/.terraform
|
||||
|
||||
.terraform_base:
|
||||
image: hashicorp/terraform:1.6
|
||||
before_script:
|
||||
- cd ${TF_ROOT}
|
||||
- terraform init -input=false
|
||||
|
||||
# Stage 1: Validate
|
||||
terraform_fmt:
|
||||
extends: .terraform_base
|
||||
stage: validate
|
||||
script:
|
||||
- terraform fmt -check -recursive
|
||||
allow_failure: false
|
||||
|
||||
terraform_validate:
|
||||
extends: .terraform_base
|
||||
stage: validate
|
||||
script:
|
||||
- terraform validate
|
||||
allow_failure: false
|
||||
|
||||
# Stage 2: Plan
|
||||
terraform_plan:
|
||||
extends: .terraform_base
|
||||
stage: plan
|
||||
script:
|
||||
- terraform plan -out=${TF_PLAN_FILE} -input=false
|
||||
- terraform show -json ${TF_PLAN_FILE} > ${TF_PLAN_JSON}
|
||||
artifacts:
|
||||
name: "terraform-plan-${CI_COMMIT_SHORT_SHA}"
|
||||
paths:
|
||||
- ${TF_ROOT}/${TF_PLAN_FILE}
|
||||
- ${TF_ROOT}/${TF_PLAN_JSON}
|
||||
expire_in: 7 days
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
# Stage 3: Manual Approval Gate
|
||||
manual_approval:
|
||||
stage: approve
|
||||
script:
|
||||
- echo "Terraform plan approved by ${GITLAB_USER_NAME}"
|
||||
when: manual
|
||||
allow_failure: false
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
needs:
|
||||
- terraform_plan
|
||||
|
||||
# Stage 4: Apply
|
||||
terraform_apply:
|
||||
extends: .terraform_base
|
||||
stage: apply
|
||||
script:
|
||||
- terraform apply -input=false ${TF_PLAN_FILE}
|
||||
- terraform output -json > terraform_outputs.json
|
||||
artifacts:
|
||||
name: "terraform-outputs-${CI_COMMIT_SHORT_SHA}"
|
||||
paths:
|
||||
- ${TF_ROOT}/terraform_outputs.json
|
||||
expire_in: 30 days
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
needs:
|
||||
- manual_approval
|
||||
environment:
|
||||
name: production
|
||||
action: start
|
||||
|
||||
# Stage 5: Compliance
|
||||
compliance_report:
|
||||
stage: compliance
|
||||
image: python:3.11-slim
|
||||
before_script:
|
||||
- pip install blake3
|
||||
script:
|
||||
- |
|
||||
# Generate compliance snapshot
|
||||
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
COMMIT_SHA=${CI_COMMIT_SHA}
|
||||
|
||||
# Hash all terraform files
|
||||
find ${TF_ROOT} -name "*.tf" -exec cat {} \; | python3 -c "
|
||||
import sys
|
||||
import blake3
|
||||
import json
|
||||
|
||||
content = sys.stdin.read()
|
||||
tf_hash = blake3.blake3(content.encode()).hexdigest()
|
||||
|
||||
receipt = {
|
||||
'receipt_type': 'terraform_compliance',
|
||||
'schema_version': 'vm_tf_compliance_v1',
|
||||
'timestamp': '${TIMESTAMP}',
|
||||
'commit_sha': '${COMMIT_SHA}',
|
||||
'pipeline_id': '${CI_PIPELINE_ID}',
|
||||
'job_id': '${CI_JOB_ID}',
|
||||
'tf_files_hash': tf_hash,
|
||||
'applied_by': '${GITLAB_USER_NAME}',
|
||||
'environment': 'production'
|
||||
}
|
||||
|
||||
print(json.dumps(receipt, indent=2))
|
||||
" > compliance_receipt.json
|
||||
|
||||
cat compliance_receipt.json
|
||||
artifacts:
|
||||
name: "compliance-${CI_COMMIT_SHORT_SHA}"
|
||||
paths:
|
||||
- compliance_receipt.json
|
||||
- ${TF_ROOT}/terraform_outputs.json
|
||||
expire_in: 365 days
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
needs:
|
||||
- terraform_apply
|
||||
|
||||
# Merge Request: Plan Only
|
||||
mr_plan:
|
||||
extends: .terraform_base
|
||||
stage: plan
|
||||
script:
|
||||
- terraform plan -input=false -no-color -out=plan.tfplan | tee plan_output.txt
|
||||
- terraform show -json plan.tfplan > plan.json
|
||||
artifacts:
|
||||
paths:
|
||||
- ${TF_ROOT}/plan_output.txt
|
||||
- ${TF_ROOT}/plan.tfplan
|
||||
- ${TF_ROOT}/plan.json
|
||||
expire_in: 7 days
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
# ==============================================================================
|
||||
# PHASE 6 - GITOPS PR WORKFLOWS
|
||||
# ==============================================================================
|
||||
|
||||
# Post plan summary as MR comment
|
||||
gitops:plan_comment:
|
||||
stage: gitops
|
||||
image: python:3.12-slim
|
||||
before_script:
|
||||
- pip install requests pyyaml
|
||||
script:
|
||||
- |
|
||||
cd ${CI_PROJECT_DIR}/gitops
|
||||
python3 ci_plan_comment.py
|
||||
variables:
|
||||
GITLAB_TOKEN: ${GITLAB_TOKEN}
|
||||
artifacts:
|
||||
paths:
|
||||
- plan_output.env
|
||||
reports:
|
||||
dotenv: plan_output.env
|
||||
expire_in: 1 day
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
needs:
|
||||
- mr_plan
|
||||
|
||||
# Drift remediation (scheduled or alert-triggered)
|
||||
gitops:drift_remediation:
|
||||
stage: gitops
|
||||
image: python:3.12-slim
|
||||
before_script:
|
||||
- pip install requests pyyaml
|
||||
- apt-get update && apt-get install -y git
|
||||
- git config --global user.email "gitops-bot@cloudflare-mesh.local"
|
||||
- git config --global user.name "GitOps Bot"
|
||||
script:
|
||||
- |
|
||||
cd ${CI_PROJECT_DIR}/gitops
|
||||
python3 drift_pr_bot.py \
|
||||
--trigger-source "${GITOPS_TRIGGER_SOURCE:-scheduled}"
|
||||
variables:
|
||||
GITLAB_TOKEN: ${GITLAB_TOKEN}
|
||||
GITOPS_DRY_RUN: "false"
|
||||
rules:
|
||||
# Scheduled runs
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule" && $GITOPS_DRIFT_CHECK == "true"
|
||||
# Alert-triggered runs
|
||||
- if: $CI_PIPELINE_SOURCE == "trigger" && $GITOPS_TRIGGER_SOURCE == "alert"
|
||||
needs: []
|
||||
|
||||
# Risk gate - block high-risk changes without approval
|
||||
gitops:risk_gate:
|
||||
stage: gitops
|
||||
image: python:3.12-slim
|
||||
before_script:
|
||||
- pip install pyyaml
|
||||
script:
|
||||
- |
|
||||
cd ${CI_PROJECT_DIR}/gitops
|
||||
RISK=$(python3 plan_summarizer.py --format json | python3 -c "import sys,json; print(json.load(sys.stdin)['overall_risk'])")
|
||||
echo "Overall risk level: $RISK"
|
||||
|
||||
if [ "$RISK" = "CRITICAL" ]; then
|
||||
echo "CRITICAL risk detected. Manual approval required."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Risk level acceptable for auto-merge consideration."
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
needs:
|
||||
- mr_plan
|
||||
allow_failure: true
|
||||
|
||||
# Stage 6: Deep Binding - State Reconciliation
|
||||
state_reconcile:
|
||||
stage: reconcile
|
||||
image: python:3.11-slim
|
||||
variables:
|
||||
SCRIPTS_DIR: ${CI_PROJECT_DIR}/../scripts
|
||||
before_script:
|
||||
- pip install requests
|
||||
script:
|
||||
- |
|
||||
echo "=== Cloudflare State Reconciliation ==="
|
||||
|
||||
# Run state reconciler
|
||||
python3 ${CI_PROJECT_DIR}/../scripts/state-reconciler.py \
|
||||
--zone-id ${CLOUDFLARE_ZONE_ID} \
|
||||
--account-id ${CLOUDFLARE_ACCOUNT_ID} \
|
||||
--output-dir ${CI_PROJECT_DIR}/../snapshots \
|
||||
--receipt-dir ${CI_PROJECT_DIR}/../receipts
|
||||
|
||||
# Find latest snapshot
|
||||
SNAPSHOT=$(ls -t ${CI_PROJECT_DIR}/../snapshots/cloudflare-*.json | head -1)
|
||||
echo "Snapshot: $SNAPSHOT"
|
||||
|
||||
# Run invariant checker
|
||||
python3 ${CI_PROJECT_DIR}/../scripts/invariant-checker.py \
|
||||
--snapshot "$SNAPSHOT" \
|
||||
--output-dir ${CI_PROJECT_DIR}/../anomalies || INVARIANT_FAILED=1
|
||||
|
||||
# Find latest report
|
||||
REPORT=$(ls -t ${CI_PROJECT_DIR}/../anomalies/invariant-report-*.json | head -1)
|
||||
echo "Report: $REPORT"
|
||||
|
||||
# Copy artifacts
|
||||
mkdir -p reconcile_artifacts
|
||||
cp "$SNAPSHOT" reconcile_artifacts/ 2>/dev/null || true
|
||||
cp "$REPORT" reconcile_artifacts/ 2>/dev/null || true
|
||||
cp ${CI_PROJECT_DIR}/../anomalies/anomaly-*.json reconcile_artifacts/ 2>/dev/null || true
|
||||
|
||||
# Summary
|
||||
python3 -c "
|
||||
import json
|
||||
with open('$REPORT') as f:
|
||||
r = json.load(f)
|
||||
print(f\"Passed: {r['summary']['passed']}\")
|
||||
print(f\"Failed: {r['summary']['failed']}\")
|
||||
"
|
||||
|
||||
if [ "${INVARIANT_FAILED:-0}" = "1" ]; then
|
||||
echo "WARNING: Invariant failures detected"
|
||||
exit 1
|
||||
fi
|
||||
artifacts:
|
||||
name: "reconcile-${CI_COMMIT_SHORT_SHA}"
|
||||
paths:
|
||||
- reconcile_artifacts/
|
||||
expire_in: 365 days
|
||||
when: always
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
needs:
|
||||
- compliance_report
|
||||
allow_failure: true
|
||||
|
||||
# Scheduled Reconciliation (Daily)
|
||||
scheduled_reconcile:
|
||||
extends: state_reconcile
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
needs: []
|
||||
|
||||
# Monthly Tunnel Rotation
|
||||
monthly_rotation:
|
||||
stage: reconcile
|
||||
image: python:3.11-slim
|
||||
before_script:
|
||||
- pip install requests
|
||||
script:
|
||||
- |
|
||||
echo "=== Monthly Tunnel Rotation ==="
|
||||
python3 ${CI_PROJECT_DIR}/../scripts/tunnel-rotation-scheduler.py \
|
||||
--account-id ${CLOUDFLARE_ACCOUNT_ID} \
|
||||
--zone-id ${CLOUDFLARE_ZONE_ID} \
|
||||
--max-age 90 \
|
||||
--output-dir ${CI_PROJECT_DIR}/../receipts
|
||||
artifacts:
|
||||
name: "rotation-${CI_COMMIT_SHORT_SHA}"
|
||||
paths:
|
||||
- receipts/tunnel-rotation-*.json
|
||||
expire_in: 365 days
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule" && $ROTATION_CYCLE == "monthly"
|
||||
needs: []
|
||||
|
||||
# ProofChain Anchor (Post-Apply)
|
||||
proofchain_anchor:
|
||||
stage: reconcile
|
||||
image: python:3.11-slim
|
||||
before_script:
|
||||
- pip install requests
|
||||
script:
|
||||
- |
|
||||
echo "=== ProofChain Anchoring ==="
|
||||
|
||||
# Run full anchor workflow
|
||||
bash ${CI_PROJECT_DIR}/../scripts/anchor-cloudflare-state.sh \
|
||||
--zone-id ${CLOUDFLARE_ZONE_ID} \
|
||||
--account-id ${CLOUDFLARE_ACCOUNT_ID}
|
||||
|
||||
# Copy artifacts
|
||||
mkdir -p anchor_artifacts
|
||||
cp ${CI_PROJECT_DIR}/../snapshots/*.json anchor_artifacts/ 2>/dev/null || true
|
||||
cp ${CI_PROJECT_DIR}/../receipts/*.json anchor_artifacts/ 2>/dev/null || true
|
||||
cp ${CI_PROJECT_DIR}/../anomalies/*.json anchor_artifacts/ 2>/dev/null || true
|
||||
cp ${CI_PROJECT_DIR}/../proofchain-anchors.jsonl anchor_artifacts/ 2>/dev/null || true
|
||||
|
||||
echo "Anchoring complete"
|
||||
artifacts:
|
||||
name: "anchor-${CI_COMMIT_SHORT_SHA}"
|
||||
paths:
|
||||
- anchor_artifacts/
|
||||
expire_in: 365 days
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
needs:
|
||||
- terraform_apply
|
||||
allow_failure: true
|
||||
80
terraform/README.md
Normal file
80
terraform/README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Cloudflare Terraform Configuration
|
||||
|
||||
Infrastructure as Code for VaultMesh and OffSec Cloudflare resources.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Terraform >= 1.0
|
||||
2. Cloudflare API token with permissions:
|
||||
- Zone: Edit
|
||||
- DNS: Edit
|
||||
- Access: Edit
|
||||
- Argo Tunnel: Edit
|
||||
- WAF: Edit
|
||||
|
||||
## Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `main.tf` | Provider configuration |
|
||||
| `variables.tf` | Input variables |
|
||||
| `zones.tf` | Zone creation and settings |
|
||||
| `dns.tf` | DNS records |
|
||||
| `waf.tf` | WAF and firewall rules |
|
||||
| `tunnels.tf` | Cloudflare Tunnels |
|
||||
| `access.tf` | Zero Trust Access apps |
|
||||
| `outputs.tf` | Output values |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Initialize
|
||||
terraform init
|
||||
|
||||
# Create terraform.tfvars
|
||||
cat > terraform.tfvars <<EOF
|
||||
cloudflare_api_token = "your-api-token"
|
||||
cloudflare_account_name = "your-account-name"
|
||||
tunnel_secret_vaultmesh = "base64-encoded-secret"
|
||||
tunnel_secret_offsec = "base64-encoded-secret"
|
||||
admin_emails = ["admin@vaultmesh.org"]
|
||||
EOF
|
||||
|
||||
# Plan
|
||||
terraform plan
|
||||
|
||||
# Apply
|
||||
terraform apply
|
||||
```
|
||||
|
||||
## Generate Tunnel Secrets
|
||||
|
||||
```bash
|
||||
# Generate 32-byte random secret, base64 encoded
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
## Domains Managed
|
||||
|
||||
- vaultmesh.org
|
||||
- offsec.global
|
||||
- offsecglobal.com
|
||||
- offsecagent.com
|
||||
- offsecshield.com
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Never commit `terraform.tfvars` to git
|
||||
- Use environment variables for CI/CD:
|
||||
```bash
|
||||
export TF_VAR_cloudflare_api_token="..."
|
||||
```
|
||||
- Rotate tunnel secrets every 90 days
|
||||
- Review Access policies regularly
|
||||
|
||||
## VaultMesh Integration
|
||||
|
||||
After applying, emit a VaultMesh receipt:
|
||||
```bash
|
||||
terraform output -json > /var/lib/vaultmesh/snapshots/cloudflare-$(date +%Y%m%d).json
|
||||
```
|
||||
122
terraform/access.tf
Normal file
122
terraform/access.tf
Normal file
@@ -0,0 +1,122 @@
|
||||
# Cloudflare Access - Zero Trust Applications
|
||||
|
||||
# Access Application for VaultMesh Dashboard
|
||||
resource "cloudflare_access_application" "vaultmesh_dash" {
|
||||
zone_id = cloudflare_zone.domains["vaultmesh.org"].id
|
||||
name = "VaultMesh Dashboard"
|
||||
domain = "dash.vaultmesh.org"
|
||||
type = "self_hosted"
|
||||
session_duration = "24h"
|
||||
auto_redirect_to_identity = true
|
||||
|
||||
allowed_idps = var.allowed_idps
|
||||
}
|
||||
|
||||
# Access Application for VaultMesh Guardian (Admin)
|
||||
resource "cloudflare_access_application" "vaultmesh_guardian" {
|
||||
zone_id = cloudflare_zone.domains["vaultmesh.org"].id
|
||||
name = "VaultMesh Guardian"
|
||||
domain = "guardian.vaultmesh.org"
|
||||
type = "self_hosted"
|
||||
session_duration = "8h" # Shorter for admin
|
||||
auto_redirect_to_identity = true
|
||||
|
||||
allowed_idps = var.allowed_idps
|
||||
}
|
||||
|
||||
# Access Application for OffSec Internal
|
||||
resource "cloudflare_access_application" "offsec_internal" {
|
||||
zone_id = cloudflare_zone.domains["offsec.global"].id
|
||||
name = "OffSec Internal Tools"
|
||||
domain = "internal.offsec.global"
|
||||
type = "self_hosted"
|
||||
session_duration = "12h"
|
||||
auto_redirect_to_identity = true
|
||||
|
||||
allowed_idps = var.allowed_idps
|
||||
}
|
||||
|
||||
# Access Policy - Allow specific emails
|
||||
resource "cloudflare_access_policy" "vaultmesh_dash_policy" {
|
||||
application_id = cloudflare_access_application.vaultmesh_dash.id
|
||||
zone_id = cloudflare_zone.domains["vaultmesh.org"].id
|
||||
name = "Allow VaultMesh Team"
|
||||
precedence = 1
|
||||
decision = "allow"
|
||||
|
||||
include {
|
||||
email_domain = var.allowed_email_domains
|
||||
}
|
||||
|
||||
require {
|
||||
# Require MFA
|
||||
auth_method = "mfa"
|
||||
}
|
||||
}
|
||||
|
||||
# Access Policy - Guardian (more restrictive)
|
||||
resource "cloudflare_access_policy" "vaultmesh_guardian_policy" {
|
||||
application_id = cloudflare_access_application.vaultmesh_guardian.id
|
||||
zone_id = cloudflare_zone.domains["vaultmesh.org"].id
|
||||
name = "Allow Guardian Admins"
|
||||
precedence = 1
|
||||
decision = "allow"
|
||||
|
||||
include {
|
||||
email = var.admin_emails
|
||||
}
|
||||
|
||||
require {
|
||||
# Require hardware key MFA
|
||||
auth_method = "mfa"
|
||||
}
|
||||
}
|
||||
|
||||
# Access Policy - OffSec Internal
|
||||
resource "cloudflare_access_policy" "offsec_internal_policy" {
|
||||
application_id = cloudflare_access_application.offsec_internal.id
|
||||
zone_id = cloudflare_zone.domains["offsec.global"].id
|
||||
name = "Allow OffSec Team"
|
||||
precedence = 1
|
||||
decision = "allow"
|
||||
|
||||
include {
|
||||
email_domain = var.allowed_email_domains
|
||||
}
|
||||
|
||||
require {
|
||||
auth_method = "mfa"
|
||||
}
|
||||
}
|
||||
|
||||
# Service Tokens for machine-to-machine auth
|
||||
resource "cloudflare_access_service_token" "vaultmesh_api" {
|
||||
zone_id = cloudflare_zone.domains["vaultmesh.org"].id
|
||||
name = "VaultMesh API Service Token"
|
||||
min_days_for_renewal = 30
|
||||
}
|
||||
|
||||
resource "cloudflare_access_service_token" "offsec_api" {
|
||||
zone_id = cloudflare_zone.domains["offsec.global"].id
|
||||
name = "OffSec API Service Token"
|
||||
min_days_for_renewal = 30
|
||||
}
|
||||
|
||||
# Variables for Access
|
||||
variable "allowed_idps" {
|
||||
description = "List of allowed Identity Provider IDs"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "allowed_email_domains" {
|
||||
description = "Email domains allowed to access applications"
|
||||
type = list(string)
|
||||
default = ["vaultmesh.org", "offsec.global"]
|
||||
}
|
||||
|
||||
variable "admin_emails" {
|
||||
description = "Specific admin email addresses for sensitive apps"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
73
terraform/dns.tf
Normal file
73
terraform/dns.tf
Normal file
@@ -0,0 +1,73 @@
|
||||
# DNS Records for each zone
|
||||
# Root A record (proxied) - points to tunnel or origin
|
||||
resource "cloudflare_record" "root_a" {
|
||||
for_each = cloudflare_zone.domains
|
||||
zone_id = each.value.id
|
||||
name = "@"
|
||||
value = var.origin_ip
|
||||
type = "A"
|
||||
proxied = true
|
||||
ttl = 1 # Auto when proxied
|
||||
}
|
||||
|
||||
# WWW CNAME
|
||||
resource "cloudflare_record" "www" {
|
||||
for_each = cloudflare_zone.domains
|
||||
zone_id = each.value.id
|
||||
name = "www"
|
||||
value = each.key
|
||||
type = "CNAME"
|
||||
proxied = true
|
||||
ttl = 1
|
||||
}
|
||||
|
||||
# SPF Record
|
||||
resource "cloudflare_record" "spf" {
|
||||
for_each = cloudflare_zone.domains
|
||||
zone_id = each.value.id
|
||||
name = "@"
|
||||
content = "v=spf1 include:_spf.mx.cloudflare.net -all"
|
||||
type = "TXT"
|
||||
ttl = 3600
|
||||
}
|
||||
|
||||
# DMARC Record
|
||||
resource "cloudflare_record" "dmarc" {
|
||||
for_each = cloudflare_zone.domains
|
||||
zone_id = each.value.id
|
||||
name = "_dmarc"
|
||||
value = "v=DMARC1; p=reject; rua=mailto:dmarc@${each.key}"
|
||||
type = "TXT"
|
||||
ttl = 3600
|
||||
}
|
||||
|
||||
# MX Records (using Cloudflare Email Routing or custom)
|
||||
resource "cloudflare_record" "mx_primary" {
|
||||
for_each = cloudflare_zone.domains
|
||||
zone_id = each.value.id
|
||||
name = "@"
|
||||
value = "route1.mx.cloudflare.net"
|
||||
type = "MX"
|
||||
priority = 10
|
||||
ttl = 3600
|
||||
}
|
||||
|
||||
resource "cloudflare_record" "mx_secondary" {
|
||||
for_each = cloudflare_zone.domains
|
||||
zone_id = each.value.id
|
||||
name = "@"
|
||||
value = "route2.mx.cloudflare.net"
|
||||
type = "MX"
|
||||
priority = 20
|
||||
ttl = 3600
|
||||
}
|
||||
|
||||
resource "cloudflare_record" "mx_tertiary" {
|
||||
for_each = cloudflare_zone.domains
|
||||
zone_id = each.value.id
|
||||
name = "@"
|
||||
value = "route3.mx.cloudflare.net"
|
||||
type = "MX"
|
||||
priority = 30
|
||||
ttl = 3600
|
||||
}
|
||||
29
terraform/main.tf
Normal file
29
terraform/main.tf
Normal file
@@ -0,0 +1,29 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "~> 4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "cloudflare" {
|
||||
api_token = var.cloudflare_api_token
|
||||
}
|
||||
|
||||
# Data source for account (optional - fails gracefully)
|
||||
data "cloudflare_accounts" "main" {
|
||||
count = var.cloudflare_account_name != "" ? 1 : 0
|
||||
name = var.cloudflare_account_name
|
||||
}
|
||||
|
||||
locals {
|
||||
# Use account ID from data source if available, otherwise use variable
|
||||
account_id = (
|
||||
var.cloudflare_account_name != "" && length(data.cloudflare_accounts.main) > 0 && length(data.cloudflare_accounts.main[0].accounts) > 0
|
||||
? data.cloudflare_accounts.main[0].accounts[0].id
|
||||
: var.cloudflare_account_id
|
||||
)
|
||||
}
|
||||
57
terraform/outputs.tf
Normal file
57
terraform/outputs.tf
Normal file
@@ -0,0 +1,57 @@
|
||||
# Outputs
|
||||
|
||||
output "zone_ids" {
|
||||
description = "Map of domain names to zone IDs"
|
||||
value = {
|
||||
for domain, zone in cloudflare_zone.domains : domain => zone.id
|
||||
}
|
||||
}
|
||||
|
||||
output "zone_name_servers" {
|
||||
description = "Name servers for each zone"
|
||||
value = {
|
||||
for domain, zone in cloudflare_zone.domains : domain => zone.name_servers
|
||||
}
|
||||
}
|
||||
|
||||
output "tunnel_ids" {
|
||||
description = "Tunnel IDs"
|
||||
value = {
|
||||
vaultmesh = cloudflare_tunnel.vaultmesh.id
|
||||
offsec = cloudflare_tunnel.offsec.id
|
||||
}
|
||||
}
|
||||
|
||||
output "tunnel_cnames" {
|
||||
description = "Tunnel CNAME targets"
|
||||
value = {
|
||||
vaultmesh = "${cloudflare_tunnel.vaultmesh.id}.cfargotunnel.com"
|
||||
offsec = "${cloudflare_tunnel.offsec.id}.cfargotunnel.com"
|
||||
}
|
||||
}
|
||||
|
||||
output "access_application_ids" {
|
||||
description = "Access Application IDs"
|
||||
value = {
|
||||
vaultmesh_dash = cloudflare_access_application.vaultmesh_dash.id
|
||||
vaultmesh_guardian = cloudflare_access_application.vaultmesh_guardian.id
|
||||
offsec_internal = cloudflare_access_application.offsec_internal.id
|
||||
}
|
||||
}
|
||||
|
||||
output "service_token_client_ids" {
|
||||
description = "Service token client IDs (secrets are sensitive)"
|
||||
value = {
|
||||
vaultmesh_api = cloudflare_access_service_token.vaultmesh_api.client_id
|
||||
offsec_api = cloudflare_access_service_token.offsec_api.client_id
|
||||
}
|
||||
}
|
||||
|
||||
output "service_token_secrets" {
|
||||
description = "Service token secrets"
|
||||
value = {
|
||||
vaultmesh_api = cloudflare_access_service_token.vaultmesh_api.client_secret
|
||||
offsec_api = cloudflare_access_service_token.offsec_api.client_secret
|
||||
}
|
||||
sensitive = true
|
||||
}
|
||||
3
terraform/terraform.tfvars
Normal file
3
terraform/terraform.tfvars
Normal file
@@ -0,0 +1,3 @@
|
||||
cloudflare_api_token = "placeholder-token"
|
||||
cloudflare_account_id = "placeholder-account-id"
|
||||
cloudflare_account_name = "" # Leave empty to use hardcoded account_id
|
||||
121
terraform/tunnels.tf
Normal file
121
terraform/tunnels.tf
Normal file
@@ -0,0 +1,121 @@
|
||||
# Cloudflare Tunnels
|
||||
|
||||
# Tunnel for VaultMesh services
|
||||
resource "cloudflare_tunnel" "vaultmesh" {
|
||||
account_id = local.account_id
|
||||
name = "vaultmesh-tunnel"
|
||||
secret = var.tunnel_secret_vaultmesh
|
||||
}
|
||||
|
||||
# Tunnel for OffSec services
|
||||
resource "cloudflare_tunnel" "offsec" {
|
||||
account_id = local.account_id
|
||||
name = "offsec-tunnel"
|
||||
secret = var.tunnel_secret_offsec
|
||||
}
|
||||
|
||||
# Tunnel configuration for VaultMesh
|
||||
resource "cloudflare_tunnel_config" "vaultmesh" {
|
||||
account_id = local.account_id
|
||||
tunnel_id = cloudflare_tunnel.vaultmesh.id
|
||||
|
||||
config {
|
||||
# VaultMesh Core API
|
||||
ingress_rule {
|
||||
hostname = "api.vaultmesh.org"
|
||||
service = "http://localhost:8080"
|
||||
origin_request {
|
||||
connect_timeout = "10s"
|
||||
no_tls_verify = false
|
||||
}
|
||||
}
|
||||
|
||||
# VaultMesh Dashboard
|
||||
ingress_rule {
|
||||
hostname = "dash.vaultmesh.org"
|
||||
service = "http://localhost:3000"
|
||||
}
|
||||
|
||||
# VaultMesh Guardian
|
||||
ingress_rule {
|
||||
hostname = "guardian.vaultmesh.org"
|
||||
service = "http://localhost:8081"
|
||||
}
|
||||
|
||||
# Catch-all
|
||||
ingress_rule {
|
||||
service = "http_status:404"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Tunnel configuration for OffSec
|
||||
resource "cloudflare_tunnel_config" "offsec" {
|
||||
account_id = local.account_id
|
||||
tunnel_id = cloudflare_tunnel.offsec.id
|
||||
|
||||
config {
|
||||
# OffSec main site
|
||||
ingress_rule {
|
||||
hostname = "offsec.global"
|
||||
service = "http://localhost:8090"
|
||||
}
|
||||
|
||||
# OffSec Agent portal
|
||||
ingress_rule {
|
||||
hostname = "offsecagent.com"
|
||||
service = "http://localhost:8091"
|
||||
}
|
||||
|
||||
# OffSec Shield dashboard
|
||||
ingress_rule {
|
||||
hostname = "offsecshield.com"
|
||||
service = "http://localhost:8092"
|
||||
}
|
||||
|
||||
# Catch-all
|
||||
ingress_rule {
|
||||
service = "http_status:404"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# DNS records pointing to tunnels
|
||||
resource "cloudflare_record" "tunnel_vaultmesh_api" {
|
||||
zone_id = cloudflare_zone.domains["vaultmesh.org"].id
|
||||
name = "api"
|
||||
value = "${cloudflare_tunnel.vaultmesh.id}.cfargotunnel.com"
|
||||
type = "CNAME"
|
||||
proxied = true
|
||||
}
|
||||
|
||||
resource "cloudflare_record" "tunnel_vaultmesh_dash" {
|
||||
zone_id = cloudflare_zone.domains["vaultmesh.org"].id
|
||||
name = "dash"
|
||||
value = "${cloudflare_tunnel.vaultmesh.id}.cfargotunnel.com"
|
||||
type = "CNAME"
|
||||
proxied = true
|
||||
}
|
||||
|
||||
resource "cloudflare_record" "tunnel_vaultmesh_guardian" {
|
||||
zone_id = cloudflare_zone.domains["vaultmesh.org"].id
|
||||
name = "guardian"
|
||||
value = "${cloudflare_tunnel.vaultmesh.id}.cfargotunnel.com"
|
||||
type = "CNAME"
|
||||
proxied = true
|
||||
}
|
||||
|
||||
# Variables for tunnel secrets
|
||||
variable "tunnel_secret_vaultmesh" {
|
||||
description = "Secret for VaultMesh tunnel (base64 encoded 32+ bytes)"
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "tunnel_secret_offsec" {
|
||||
description = "Secret for OffSec tunnel (base64 encoded 32+ bytes)"
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
66
terraform/variables.tf
Normal file
66
terraform/variables.tf
Normal file
@@ -0,0 +1,66 @@
|
||||
variable "cloudflare_api_token" {
|
||||
description = "Cloudflare API token with Zone:Edit, DNS:Edit, Access:Edit permissions"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "cloudflare_account_name" {
|
||||
description = "Cloudflare account name"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "cloudflare_account_id" {
|
||||
description = "Cloudflare account ID (used if account name lookup fails)"
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "domains" {
|
||||
description = "Map of domains to manage"
|
||||
type = map(object({
|
||||
plan = string
|
||||
jump_start = bool
|
||||
}))
|
||||
default = {
|
||||
"offsec.global" = {
|
||||
plan = "free"
|
||||
jump_start = false
|
||||
}
|
||||
"offsecglobal.com" = {
|
||||
plan = "free"
|
||||
jump_start = false
|
||||
}
|
||||
"offsecagent.com" = {
|
||||
plan = "free"
|
||||
jump_start = false
|
||||
}
|
||||
"offsecshield.com" = {
|
||||
plan = "free"
|
||||
jump_start = false
|
||||
}
|
||||
"vaultmesh.org" = {
|
||||
plan = "free"
|
||||
jump_start = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "origin_ip" {
|
||||
description = "Origin server IP (should be tunnel, but fallback)"
|
||||
type = string
|
||||
default = "192.0.2.1" # Placeholder - use tunnel instead
|
||||
}
|
||||
|
||||
variable "trusted_admin_ips" {
|
||||
description = "List of trusted admin IP addresses"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "blocked_countries" {
|
||||
description = "Countries to challenge/block"
|
||||
type = list(string)
|
||||
default = ["CN", "RU", "KP", "IR"]
|
||||
}
|
||||
91
terraform/waf.tf
Normal file
91
terraform/waf.tf
Normal file
@@ -0,0 +1,91 @@
|
||||
# WAF Rulesets and Firewall Rules
|
||||
|
||||
# Block non-HTTPS (should be handled by always_use_https, but explicit rule)
|
||||
resource "cloudflare_ruleset" "security_rules" {
|
||||
for_each = cloudflare_zone.domains
|
||||
zone_id = each.value.id
|
||||
name = "Security Rules"
|
||||
kind = "zone"
|
||||
phase = "http_request_firewall_custom"
|
||||
|
||||
# Rule 1: Block requests to /admin from non-trusted IPs
|
||||
rules {
|
||||
action = "block"
|
||||
expression = "(http.request.uri.path contains \"/admin\") and not (ip.src in {${join(" ", var.trusted_admin_ips)}})"
|
||||
description = "Block admin access from untrusted IPs"
|
||||
enabled = length(var.trusted_admin_ips) > 0
|
||||
}
|
||||
|
||||
# Rule 2: Challenge suspicious countries
|
||||
rules {
|
||||
action = "managed_challenge"
|
||||
expression = "(ip.src.country in {\"${join("\" \"", var.blocked_countries)}\"})"
|
||||
description = "Challenge traffic from high-risk countries"
|
||||
enabled = true
|
||||
}
|
||||
|
||||
# Rule 3: Block known bad user agents
|
||||
rules {
|
||||
action = "block"
|
||||
expression = "(http.user_agent contains \"sqlmap\") or (http.user_agent contains \"nikto\") or (http.user_agent contains \"nmap\")"
|
||||
description = "Block known scanning tools"
|
||||
enabled = true
|
||||
}
|
||||
|
||||
# Rule 4: Rate limit API endpoints
|
||||
rules {
|
||||
action = "block"
|
||||
ratelimit {
|
||||
characteristics = ["ip.src"]
|
||||
period = 10
|
||||
requests_per_period = 30
|
||||
mitigation_timeout = 60
|
||||
}
|
||||
expression = "(http.request.uri.path starts_with \"/api/\")"
|
||||
description = "Rate limit API endpoints"
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
# Enable Cloudflare Managed WAF Ruleset
|
||||
resource "cloudflare_ruleset" "managed_waf" {
|
||||
for_each = cloudflare_zone.domains
|
||||
zone_id = each.value.id
|
||||
name = "Managed WAF"
|
||||
kind = "zone"
|
||||
phase = "http_request_firewall_managed"
|
||||
|
||||
# Cloudflare Managed Ruleset
|
||||
rules {
|
||||
action = "execute"
|
||||
action_parameters {
|
||||
id = "efb7b8c949ac4650a09736fc376e9aee" # Cloudflare Managed Ruleset
|
||||
}
|
||||
expression = "true"
|
||||
description = "Execute Cloudflare Managed Ruleset"
|
||||
enabled = true
|
||||
}
|
||||
|
||||
# OWASP Core Ruleset
|
||||
rules {
|
||||
action = "execute"
|
||||
action_parameters {
|
||||
id = "4814384a9e5d4991b9815dcfc25d2f1f" # OWASP Core Ruleset
|
||||
}
|
||||
expression = "true"
|
||||
description = "Execute OWASP Core Ruleset"
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
# Bot Management (if available on plan)
|
||||
resource "cloudflare_bot_management" "domains" {
|
||||
for_each = cloudflare_zone.domains
|
||||
zone_id = each.value.id
|
||||
enable_js = true
|
||||
fight_mode = true
|
||||
sbfm_definitely_automated = "block"
|
||||
sbfm_likely_automated = "managed_challenge"
|
||||
sbfm_verified_bots = "allow"
|
||||
sbfm_static_resource_protection = false
|
||||
}
|
||||
48
terraform/zones.tf
Normal file
48
terraform/zones.tf
Normal file
@@ -0,0 +1,48 @@
|
||||
# Zone resources for each domain
|
||||
resource "cloudflare_zone" "domains" {
|
||||
for_each = var.domains
|
||||
account_id = local.account_id
|
||||
zone = each.key
|
||||
plan = each.value.plan
|
||||
jump_start = each.value.jump_start
|
||||
}
|
||||
|
||||
# Enable DNSSEC on all zones
|
||||
resource "cloudflare_zone_dnssec" "domains" {
|
||||
for_each = cloudflare_zone.domains
|
||||
zone_id = each.value.id
|
||||
}
|
||||
|
||||
# Zone settings - TLS, security, etc.
|
||||
resource "cloudflare_zone_settings_override" "domains" {
|
||||
for_each = cloudflare_zone.domains
|
||||
zone_id = each.value.id
|
||||
|
||||
settings {
|
||||
# TLS Settings
|
||||
ssl = "strict"
|
||||
min_tls_version = "1.2"
|
||||
tls_1_3 = "on"
|
||||
automatic_https_rewrites = "on"
|
||||
always_use_https = "on"
|
||||
|
||||
# Security
|
||||
security_level = "medium"
|
||||
browser_check = "on"
|
||||
|
||||
# Performance
|
||||
minify {
|
||||
css = "on"
|
||||
js = "on"
|
||||
html = "on"
|
||||
}
|
||||
brotli = "on"
|
||||
|
||||
# Caching
|
||||
browser_cache_ttl = 14400
|
||||
|
||||
# Privacy
|
||||
email_obfuscation = "on"
|
||||
server_side_exclude = "on"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user