From 5efde6fcf434c3ed3ac89acf8015e27215f1270a Mon Sep 17 00:00:00 2001 From: nogueiraguh Date: Thu, 2 Apr 2026 13:19:58 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20OCI=20deployment=20=E2=80=94=20Terrafor?= =?UTF-8?q?m=20VM=20+=20OKE=20stacks,=20OCIR,=20K8s=20manifests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - terraform-vm/: ARM VM (A1.Flex Free Tier), Docker Compose, Block Volume, LB+WAF+SSL - terraform-oke/: OKE cluster (3 nodes Intel), NodePort, LB+WAF+SSL - k8s/: namespace, deployments, services, PVC, secrets, configmap - Dockerfile.frontend: self-contained nginx+dist image - push-images.sh: multi-arch build (amd64+arm64) + push to OCIR - dns.tf: conditional OCI DNS Zone + A record (when domain available) - Both stacks 100% independent, no shared modules - .gitignore: terraform state, .terraform/, *.tfvars --- .gitignore | 43 +++ deploy/Dockerfile.frontend | 6 + deploy/k8s/configmap.yaml | 11 + deploy/k8s/deployment.yaml | 111 +++++++ deploy/k8s/namespace.yaml | 6 + deploy/k8s/pvc.yaml | 12 + deploy/k8s/secret.yaml | 35 +++ deploy/k8s/service.yaml | 42 +++ deploy/scripts/push-images.sh | 61 ++++ deploy/terraform-oke/dns.tf | 29 ++ deploy/terraform-oke/lb.tf | 157 ++++++++++ deploy/terraform-oke/network.tf | 287 ++++++++++++++++++ deploy/terraform-oke/oke.tf | 116 +++++++ deploy/terraform-oke/outputs.tf | 75 +++++ deploy/terraform-oke/provider.tf | 27 ++ deploy/terraform-oke/registry.tf | 18 ++ deploy/terraform-oke/terraform.tfvars.example | 58 ++++ deploy/terraform-oke/variables.tf | 198 ++++++++++++ deploy/terraform-oke/waf.tf | 114 +++++++ deploy/terraform-vm/compute.tf | 78 +++++ deploy/terraform-vm/dns.tf | 36 +++ deploy/terraform-vm/lb.tf | 143 +++++++++ deploy/terraform-vm/network.tf | 189 ++++++++++++ deploy/terraform-vm/outputs.tf | 56 ++++ deploy/terraform-vm/provider.tf | 22 ++ deploy/terraform-vm/registry.tf | 14 + deploy/terraform-vm/storage.tf | 22 ++ .../terraform-vm/templates/cloud-init.sh.tpl | 163 ++++++++++ deploy/terraform-vm/terraform.tfvars.example | 55 ++++ deploy/terraform-vm/variables.tf | 185 +++++++++++ deploy/terraform-vm/waf.tf | 120 ++++++++ 31 files changed, 2489 insertions(+) create mode 100644 .gitignore create mode 100644 deploy/Dockerfile.frontend create mode 100644 deploy/k8s/configmap.yaml create mode 100644 deploy/k8s/deployment.yaml create mode 100644 deploy/k8s/namespace.yaml create mode 100644 deploy/k8s/pvc.yaml create mode 100644 deploy/k8s/secret.yaml create mode 100644 deploy/k8s/service.yaml create mode 100755 deploy/scripts/push-images.sh create mode 100644 deploy/terraform-oke/dns.tf create mode 100644 deploy/terraform-oke/lb.tf create mode 100644 deploy/terraform-oke/network.tf create mode 100644 deploy/terraform-oke/oke.tf create mode 100644 deploy/terraform-oke/outputs.tf create mode 100644 deploy/terraform-oke/provider.tf create mode 100644 deploy/terraform-oke/registry.tf create mode 100644 deploy/terraform-oke/terraform.tfvars.example create mode 100644 deploy/terraform-oke/variables.tf create mode 100644 deploy/terraform-oke/waf.tf create mode 100644 deploy/terraform-vm/compute.tf create mode 100644 deploy/terraform-vm/dns.tf create mode 100644 deploy/terraform-vm/lb.tf create mode 100644 deploy/terraform-vm/network.tf create mode 100644 deploy/terraform-vm/outputs.tf create mode 100644 deploy/terraform-vm/provider.tf create mode 100644 deploy/terraform-vm/registry.tf create mode 100644 deploy/terraform-vm/storage.tf create mode 100644 deploy/terraform-vm/templates/cloud-init.sh.tpl create mode 100644 deploy/terraform-vm/terraform.tfvars.example create mode 100644 deploy/terraform-vm/variables.tf create mode 100644 deploy/terraform-vm/waf.tf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b57fbc --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Secrets +.env +.env.local +.env.*.local +*.pem +*.key +*.pk8 +*.p12 +*.jks +**/wallets/ +**/oci_configs/ + +# System +.DS_Store +**/.DS_Store + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Python +__pycache__/ +*.pyc +.venv/ + +# Node +node_modules/ + +# Build artifacts +dist/ + +# Terraform +**/.terraform/ +*.tfstate +*.tfstate.backup +*.tfvars +!*.tfvars.example +.terraform.lock.hcl + +# Project internal docs +LESSONS_LEARNED.md diff --git a/deploy/Dockerfile.frontend b/deploy/Dockerfile.frontend new file mode 100644 index 0000000..32d4950 --- /dev/null +++ b/deploy/Dockerfile.frontend @@ -0,0 +1,6 @@ +FROM nginx:alpine +RUN rm -rf /usr/share/nginx/html/* +COPY frontend-react/dist /usr/share/nginx/html/app +COPY nginx/default.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/deploy/k8s/configmap.yaml b/deploy/k8s/configmap.yaml new file mode 100644 index 0000000..f90b438 --- /dev/null +++ b/deploy/k8s/configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: oci-cis-agent-config + namespace: oci-cis-agent +data: + JWT_EXPIRY_HOURS: "12" + TZ: "America/Sao_Paulo" + DATA_DIR: "/data" + CORS_ORIGINS: "https://LOAD_BALANCER_IP" + OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING: "True" diff --git a/deploy/k8s/deployment.yaml b/deploy/k8s/deployment.yaml new file mode 100644 index 0000000..c7b4de6 --- /dev/null +++ b/deploy/k8s/deployment.yaml @@ -0,0 +1,111 @@ +## +## Backend — FastAPI (port 8000) +## +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + namespace: oci-cis-agent + labels: + app: oci-cis-agent-backend +spec: + replicas: 1 + selector: + matchLabels: + app: oci-cis-agent-backend + template: + metadata: + labels: + app: oci-cis-agent-backend + spec: + imagePullSecrets: + - name: ocir-credentials + containers: + - name: backend + image: OCIR_REGION.ocir.io/OCIR_NAMESPACE/oci-cis-agent-backend:latest + ports: + - containerPort: 8000 + envFrom: + - configMapRef: + name: oci-cis-agent-config + env: + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: app-secret + key: APP_SECRET + volumeMounts: + - name: data + mountPath: /data + resources: + requests: + memory: "2Gi" + cpu: "500m" + limits: + memory: "4Gi" + cpu: "2000m" + readinessProbe: + httpGet: + path: /api/health + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /api/health + port: 8000 + initialDelaySeconds: 60 + periodSeconds: 30 + volumes: + - name: data + persistentVolumeClaim: + claimName: oci-cis-agent-data + +--- + +## +## Frontend — nginx (port 80) +## +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + namespace: oci-cis-agent + labels: + app: oci-cis-agent-frontend +spec: + replicas: 1 + selector: + matchLabels: + app: oci-cis-agent-frontend + template: + metadata: + labels: + app: oci-cis-agent-frontend + spec: + imagePullSecrets: + - name: ocir-credentials + containers: + - name: frontend + image: OCIR_REGION.ocir.io/OCIR_NAMESPACE/oci-cis-agent-frontend:latest + ports: + - containerPort: 80 + env: + - name: TZ + valueFrom: + configMapKeyRef: + name: oci-cis-agent-config + key: TZ + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 diff --git a/deploy/k8s/namespace.yaml b/deploy/k8s/namespace.yaml new file mode 100644 index 0000000..415f463 --- /dev/null +++ b/deploy/k8s/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: oci-cis-agent + labels: + app.kubernetes.io/part-of: oci-cis-agent diff --git a/deploy/k8s/pvc.yaml b/deploy/k8s/pvc.yaml new file mode 100644 index 0000000..18268ff --- /dev/null +++ b/deploy/k8s/pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: oci-cis-agent-data + namespace: oci-cis-agent +spec: + accessModes: + - ReadWriteOnce + storageClassName: oci-bv + resources: + requests: + storage: 50Gi diff --git a/deploy/k8s/secret.yaml b/deploy/k8s/secret.yaml new file mode 100644 index 0000000..c0ce659 --- /dev/null +++ b/deploy/k8s/secret.yaml @@ -0,0 +1,35 @@ +## +## Fill in the placeholder values before applying. +## +## APP_SECRET: generate with python -c "import secrets; print(secrets.token_hex(64))" +## then base64-encode it: echo -n '' | base64 +## +## OCIR dockerconfigjson: +## kubectl create secret docker-registry ocir-credentials \ +## --namespace oci-cis-agent \ +## --docker-server=OCIR_REGION.ocir.io \ +## --docker-username='OCIR_NAMESPACE/USERNAME' \ +## --docker-password='AUTH_TOKEN' \ +## --dry-run=client -o yaml +## Copy the .data[.dockerconfigjson] value below. +## + +apiVersion: v1 +kind: Secret +metadata: + name: app-secret + namespace: oci-cis-agent +type: Opaque +data: + APP_SECRET: "" + +--- + +apiVersion: v1 +kind: Secret +metadata: + name: ocir-credentials + namespace: oci-cis-agent +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: "" diff --git a/deploy/k8s/service.yaml b/deploy/k8s/service.yaml new file mode 100644 index 0000000..f4ab6ad --- /dev/null +++ b/deploy/k8s/service.yaml @@ -0,0 +1,42 @@ +## +## Backend service — ClusterIP (internal only) +## Named "backend" so the frontend nginx proxy_pass http://backend:8000/api/ resolves via K8s DNS. +## +apiVersion: v1 +kind: Service +metadata: + name: backend + namespace: oci-cis-agent + labels: + app: oci-cis-agent-backend +spec: + type: ClusterIP + selector: + app: oci-cis-agent-backend + ports: + - port: 8000 + targetPort: 8000 + protocol: TCP + +--- + +## +## Frontend service — NodePort +## nodePort 30080 matches the OCI Load Balancer backend set. +## +apiVersion: v1 +kind: Service +metadata: + name: frontend + namespace: oci-cis-agent + labels: + app: oci-cis-agent-frontend +spec: + type: NodePort + selector: + app: oci-cis-agent-frontend + ports: + - port: 80 + targetPort: 80 + nodePort: 30080 + protocol: TCP diff --git a/deploy/scripts/push-images.sh b/deploy/scripts/push-images.sh new file mode 100755 index 0000000..0f2c8a6 --- /dev/null +++ b/deploy/scripts/push-images.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +OCIR_REGION="${OCIR_REGION:?Set OCIR_REGION (e.g., us-ashburn-1)}" +OCIR_NAMESPACE="${OCIR_NAMESPACE:?Set OCIR_NAMESPACE}" +OCIR_REPO_PREFIX="${OCIR_REPO_PREFIX:-oci-cis-agent}" +IMAGE_TAG="${IMAGE_TAG:-latest}" + +BACKEND="${OCIR_REGION}.ocir.io/${OCIR_NAMESPACE}/${OCIR_REPO_PREFIX}-backend:${IMAGE_TAG}" +FRONTEND="${OCIR_REGION}.ocir.io/${OCIR_NAMESPACE}/${OCIR_REPO_PREFIX}-frontend:${IMAGE_TAG}" + +echo "========================================" +echo "OCI CIS Agent — Build & Push to OCIR" +echo "========================================" +echo "Backend: $BACKEND" +echo "Frontend: $FRONTEND" +echo "" + +# 1. Build frontend React SPA +echo ">>> Building frontend..." +cd "$PROJECT_ROOT/frontend-react" +npm ci --silent +npm run build +echo "Frontend build OK." + +# 2. Ensure buildx builder exists +docker buildx inspect multiarch >/dev/null 2>&1 || \ + docker buildx create --name multiarch --use + +docker buildx use multiarch + +# 3. Login to OCIR +echo ">>> Logging in to OCIR..." +docker login "${OCIR_REGION}.ocir.io" + +# 4. Build + push backend (multi-arch) +echo ">>> Building & pushing backend (amd64 + arm64)..." +cd "$PROJECT_ROOT" +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t "$BACKEND" \ + --push \ + -f backend/Dockerfile backend/ + +# 5. Build + push frontend (multi-arch) +echo ">>> Building & pushing frontend (amd64 + arm64)..." +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t "$FRONTEND" \ + --push \ + -f deploy/Dockerfile.frontend . + +echo "" +echo "========================================" +echo "Done! Both images pushed." +echo "Backend: $BACKEND" +echo "Frontend: $FRONTEND" +echo "========================================" diff --git a/deploy/terraform-oke/dns.tf b/deploy/terraform-oke/dns.tf new file mode 100644 index 0000000..104d624 --- /dev/null +++ b/deploy/terraform-oke/dns.tf @@ -0,0 +1,29 @@ +# ----------------------------------------------------------------------------- +# OCI CIS Agent — OKE Deployment Stack +# DNS: Conditional Zone + A Record (only when domain_name is set) +# ----------------------------------------------------------------------------- + +resource "oci_dns_zone" "main" { + count = var.domain_name != "" ? 1 : 0 + + compartment_id = var.compartment_ocid + name = var.domain_name + zone_type = "PRIMARY" + freeform_tags = var.freeform_tags +} + +resource "oci_dns_rrset" "app_a_record" { + count = var.domain_name != "" ? 1 : 0 + + zone_name_or_id = oci_dns_zone.main[0].id + domain = var.domain_name + rtype = "A" + compartment_id = var.compartment_ocid + + items { + domain = var.domain_name + rdata = oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address + rtype = "A" + ttl = 300 + } +} diff --git a/deploy/terraform-oke/lb.tf b/deploy/terraform-oke/lb.tf new file mode 100644 index 0000000..5ab4811 --- /dev/null +++ b/deploy/terraform-oke/lb.tf @@ -0,0 +1,157 @@ +# ----------------------------------------------------------------------------- +# OCI CIS Agent — OKE Deployment Stack +# Load Balancer: Self-Signed TLS, HTTPS/HTTP Listeners, Backend Set +# ----------------------------------------------------------------------------- + +# --- Self-Signed TLS Certificate --- + +resource "tls_private_key" "lb" { + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "lb" { + private_key_pem = tls_private_key.lb.private_key_pem + + subject { + common_name = var.domain_name != "" ? var.domain_name : "oci-cis-agent.local" + organization = "OCI CIS Agent" + } + + validity_period_hours = 8760 # 1 year + is_ca_certificate = false + + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth", + ] +} + +# --- Load Balancer --- + +resource "oci_load_balancer_load_balancer" "main" { + compartment_id = var.compartment_ocid + display_name = "cisagent-oke-lb" + shape = "flexible" + + shape_details { + minimum_bandwidth_in_mbps = var.lb_min_bandwidth_mbps + maximum_bandwidth_in_mbps = var.lb_max_bandwidth_mbps + } + + subnet_ids = [oci_core_subnet.public.id] + + is_private = false + + freeform_tags = var.freeform_tags +} + +# --- Certificate --- + +resource "oci_load_balancer_certificate" "main" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + certificate_name = "cisagent-self-signed" + public_certificate = tls_self_signed_cert.lb.cert_pem + private_key = tls_private_key.lb.private_key_pem + + lifecycle { + create_before_destroy = true + } +} + +# --- Backend Set --- +# Backends point to worker node IPs on the fixed NodePort. +# The K8s Service must use spec.type=NodePort with nodePort=30080. + +resource "oci_load_balancer_backend_set" "app" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + name = "cisagent-backend-set" + policy = "ROUND_ROBIN" + + health_checker { + protocol = "HTTP" + port = var.node_port + url_path = "/api/health" + return_code = 200 + interval_ms = 10000 + timeout_in_millis = 5000 + retries = 3 + } +} + +# --- Backends (one per worker node) --- +# Uses node private IPs from the node pool data source. + +resource "oci_load_balancer_backend" "nodes" { + for_each = { + for idx, node in data.oci_containerengine_node_pool.app.nodes : + idx => node + if node.state == "ACTIVE" + } + + load_balancer_id = oci_load_balancer_load_balancer.main.id + backendset_name = oci_load_balancer_backend_set.app.name + ip_address = each.value.private_ip + port = var.node_port + weight = 1 +} + +# --- HTTPS Listener (443) --- + +resource "oci_load_balancer_listener" "https" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + name = "cisagent-https" + default_backend_set_name = oci_load_balancer_backend_set.app.name + port = 443 + protocol = "HTTP" + + ssl_configuration { + certificate_name = oci_load_balancer_certificate.main.certificate_name + verify_peer_certificate = false + protocols = ["TLSv1.2", "TLSv1.3"] + } +} + +# --- HTTP Listener (80) with redirect to HTTPS --- + +resource "oci_load_balancer_listener" "http_redirect" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + name = "cisagent-http-redirect" + default_backend_set_name = oci_load_balancer_backend_set.app.name + port = 80 + protocol = "HTTP" + rule_set_names = [oci_load_balancer_rule_set.http_redirect.name] + + connection_configuration { + idle_timeout_in_seconds = 60 + } +} + +# --- Rule Set: HTTP to HTTPS Redirect --- + +resource "oci_load_balancer_rule_set" "http_redirect" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + name = "http-to-https-redirect" + + items { + action = "REDIRECT" + description = "Redirect HTTP to HTTPS" + + conditions { + attribute_name = "PATH" + attribute_value = "/" + operator = "FORCE_LONGEST_PREFIX_MATCH" + } + + redirect_uri { + protocol = "HTTPS" + port = 443 + host = "{host}" + path = "/{path}" + query = "?{query}" + } + + response_code = 301 + } +} diff --git a/deploy/terraform-oke/network.tf b/deploy/terraform-oke/network.tf new file mode 100644 index 0000000..de61e4f --- /dev/null +++ b/deploy/terraform-oke/network.tf @@ -0,0 +1,287 @@ +# ----------------------------------------------------------------------------- +# OCI CIS Agent — OKE Deployment Stack +# Networking: VCN, Gateways, Subnets, Route Tables, Security Lists +# ----------------------------------------------------------------------------- + +# --- VCN --- +resource "oci_core_vcn" "main" { + compartment_id = var.compartment_ocid + cidr_blocks = [var.vcn_cidr] + display_name = "cisagent-oke-vcn" + dns_label = "cisagentoke" + freeform_tags = var.freeform_tags +} + +# --- Gateways --- +resource "oci_core_internet_gateway" "main" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-oke-igw" + enabled = true + freeform_tags = var.freeform_tags +} + +resource "oci_core_nat_gateway" "main" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-oke-natgw" + freeform_tags = var.freeform_tags +} + +data "oci_core_services" "all" { + filter { + name = "name" + values = ["All .* Services In Oracle Services Network"] + regex = true + } +} + +resource "oci_core_service_gateway" "main" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-oke-svcgw" + + services { + service_id = data.oci_core_services.all.services[0].id + } + + freeform_tags = var.freeform_tags +} + +# --- Route Tables --- +resource "oci_core_route_table" "public" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-oke-rt-public" + + route_rules { + network_entity_id = oci_core_internet_gateway.main.id + destination = "0.0.0.0/0" + destination_type = "CIDR_BLOCK" + } + + freeform_tags = var.freeform_tags +} + +resource "oci_core_route_table" "private" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-oke-rt-private" + + route_rules { + network_entity_id = oci_core_nat_gateway.main.id + destination = "0.0.0.0/0" + destination_type = "CIDR_BLOCK" + } + + route_rules { + network_entity_id = oci_core_service_gateway.main.id + destination = data.oci_core_services.all.services[0].cidr_block + destination_type = "SERVICE_CIDR_BLOCK" + } + + freeform_tags = var.freeform_tags +} + +# --- Security Lists --- + +# Public subnet: LB ingress on 80/443, egress all +resource "oci_core_security_list" "public" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-oke-sl-public" + + ingress_security_rules { + protocol = "6" # TCP + source = "0.0.0.0/0" + stateless = false + tcp_options { + min = 80 + max = 80 + } + } + + ingress_security_rules { + protocol = "6" + source = "0.0.0.0/0" + stateless = false + tcp_options { + min = 443 + max = 443 + } + } + + egress_security_rules { + protocol = "all" + destination = "0.0.0.0/0" + stateless = false + } + + freeform_tags = var.freeform_tags +} + +# Nodes subnet: pod-to-pod, kubelet, NodePort, SSH +resource "oci_core_security_list" "nodes" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-oke-sl-nodes" + + # Pod-to-pod communication (all traffic within nodes subnet) + ingress_security_rules { + protocol = "all" + source = var.nodes_subnet_cidr + stateless = false + } + + # Kubelet API from API endpoint subnet + ingress_security_rules { + protocol = "6" + source = var.api_subnet_cidr + stateless = false + tcp_options { + min = 10250 + max = 10250 + } + } + + # NodePort range from public subnet (LB health checks + traffic) + ingress_security_rules { + protocol = "6" + source = var.public_subnet_cidr + stateless = false + tcp_options { + min = 30000 + max = 32767 + } + } + + # SSH from VCN (for debugging) + ingress_security_rules { + protocol = "6" + source = var.vcn_cidr + stateless = false + tcp_options { + min = 22 + max = 22 + } + } + + # ICMP Path Discovery + ingress_security_rules { + protocol = "1" # ICMP + source = var.vcn_cidr + stateless = false + icmp_options { + type = 3 + code = 4 + } + } + + egress_security_rules { + protocol = "all" + destination = "0.0.0.0/0" + stateless = false + } + + freeform_tags = var.freeform_tags +} + +# API endpoint subnet: Kubernetes API access +resource "oci_core_security_list" "api_endpoint" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-oke-sl-api" + + # Kubernetes API from worker nodes + ingress_security_rules { + protocol = "6" + source = var.nodes_subnet_cidr + stateless = false + tcp_options { + min = 6443 + max = 6443 + } + } + + # Kubernetes API from VCN (kubectl access via bastion / VPN) + ingress_security_rules { + protocol = "6" + source = var.vcn_cidr + stateless = false + tcp_options { + min = 6443 + max = 6443 + } + } + + # Kubernetes API from worker nodes (12250 for OKE control plane) + ingress_security_rules { + protocol = "6" + source = var.nodes_subnet_cidr + stateless = false + tcp_options { + min = 12250 + max = 12250 + } + } + + # ICMP Path Discovery + ingress_security_rules { + protocol = "1" + source = var.vcn_cidr + stateless = false + icmp_options { + type = 3 + code = 4 + } + } + + egress_security_rules { + protocol = "all" + destination = "0.0.0.0/0" + stateless = false + } + + freeform_tags = var.freeform_tags +} + +# --- Subnets --- + +# Public subnet: Load Balancer +resource "oci_core_subnet" "public" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + cidr_block = var.public_subnet_cidr + display_name = "cisagent-oke-subnet-public" + dns_label = "publb" + prohibit_public_ip_on_vnic = false + route_table_id = oci_core_route_table.public.id + security_list_ids = [oci_core_security_list.public.id] + freeform_tags = var.freeform_tags +} + +# Private subnet: OKE worker nodes +resource "oci_core_subnet" "nodes" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + cidr_block = var.nodes_subnet_cidr + display_name = "cisagent-oke-subnet-nodes" + dns_label = "nodes" + prohibit_public_ip_on_vnic = true + route_table_id = oci_core_route_table.private.id + security_list_ids = [oci_core_security_list.nodes.id] + freeform_tags = var.freeform_tags +} + +# Private subnet: OKE API endpoint +resource "oci_core_subnet" "api_endpoint" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + cidr_block = var.api_subnet_cidr + display_name = "cisagent-oke-subnet-api" + dns_label = "api" + prohibit_public_ip_on_vnic = true + route_table_id = oci_core_route_table.private.id + security_list_ids = [oci_core_security_list.api_endpoint.id] + freeform_tags = var.freeform_tags +} diff --git a/deploy/terraform-oke/oke.tf b/deploy/terraform-oke/oke.tf new file mode 100644 index 0000000..408b784 --- /dev/null +++ b/deploy/terraform-oke/oke.tf @@ -0,0 +1,116 @@ +# ----------------------------------------------------------------------------- +# OCI CIS Agent — OKE Deployment Stack +# OKE Cluster + Node Pool +# ----------------------------------------------------------------------------- + +# --- Data Sources --- + +data "oci_identity_availability_domains" "ads" { + compartment_id = var.tenancy_ocid +} + +# Latest Oracle Linux 8 image compatible with VM.Standard3.Flex +data "oci_core_images" "oracle_linux" { + compartment_id = var.compartment_ocid + operating_system = "Oracle Linux" + operating_system_version = "8" + shape = var.node_shape + sort_by = "TIMECREATED" + sort_order = "DESC" + + filter { + name = "display_name" + values = ["^Oracle-Linux-8\\.\\d+-\\d{4}\\.\\d{2}\\.\\d{2}-\\d+$"] + regex = true + } +} + +# --- OKE Cluster --- + +resource "oci_containerengine_cluster" "main" { + compartment_id = var.compartment_ocid + kubernetes_version = var.kubernetes_version + name = var.cluster_name + vcn_id = oci_core_vcn.main.id + type = "ENHANCED_CLUSTER" + + cluster_pod_network_options { + cni_type = "FLANNEL_OVERLAY" + } + + endpoint_config { + is_public_ip_enabled = false + subnet_id = oci_core_subnet.api_endpoint.id + } + + options { + service_lb_subnet_ids = [oci_core_subnet.public.id] + + kubernetes_network_config { + pods_cidr = "10.244.0.0/16" + services_cidr = "10.96.0.0/16" + } + + persistent_volume_config { + freeform_tags = var.freeform_tags + } + + service_lb_config { + freeform_tags = var.freeform_tags + } + } + + freeform_tags = var.freeform_tags +} + +# --- Node Pool --- + +resource "oci_containerengine_node_pool" "app" { + compartment_id = var.compartment_ocid + cluster_id = oci_containerengine_cluster.main.id + kubernetes_version = var.kubernetes_version + name = "${var.cluster_name}-nodepool" + + node_shape = var.node_shape + + node_shape_config { + ocpus = var.node_ocpus + memory_in_gbs = var.node_memory_gb + } + + node_source_details { + source_type = "IMAGE" + image_id = data.oci_core_images.oracle_linux.images[0].id + boot_volume_size_in_gbs = var.node_boot_volume_gb + } + + node_config { + size = var.node_count + + # Place nodes across all available ADs for HA + dynamic "placement_configs" { + for_each = data.oci_identity_availability_domains.ads.availability_domains + content { + availability_domain = placement_configs.value.name + subnet_id = oci_core_subnet.nodes.id + } + } + + freeform_tags = var.freeform_tags + } + + ssh_public_key = var.ssh_public_key + + initial_node_labels { + key = "app" + value = "oci-cis-agent" + } + + freeform_tags = var.freeform_tags +} + +# --- Data source to retrieve node IPs after creation --- + +data "oci_containerengine_node_pool" "app" { + node_pool_id = oci_containerengine_node_pool.app.id +} diff --git a/deploy/terraform-oke/outputs.tf b/deploy/terraform-oke/outputs.tf new file mode 100644 index 0000000..20a9c58 --- /dev/null +++ b/deploy/terraform-oke/outputs.tf @@ -0,0 +1,75 @@ +# ----------------------------------------------------------------------------- +# OCI CIS Agent — OKE Deployment Stack +# Outputs +# ----------------------------------------------------------------------------- + +# --- Load Balancer --- +output "load_balancer_ip" { + description = "Public IP address of the Load Balancer" + value = oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address +} + +output "app_url" { + description = "Application URL (HTTPS)" + value = var.domain_name != "" ? "https://${var.domain_name}" : "https://${oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address}" +} + +# --- OKE Cluster --- +output "cluster_id" { + description = "OCID of the OKE cluster" + value = oci_containerengine_cluster.main.id +} + +output "cluster_name" { + description = "Name of the OKE cluster" + value = oci_containerengine_cluster.main.name +} + +output "node_pool_id" { + description = "OCID of the OKE node pool" + value = oci_containerengine_node_pool.app.id +} + +output "kubeconfig_command" { + description = "Command to generate kubeconfig for the cluster" + value = "oci ce cluster create-kubeconfig --cluster-id ${oci_containerengine_cluster.main.id} --file $HOME/.kube/config --region ${var.region} --token-version 2.0.0 --kube-endpoint PRIVATE_ENDPOINT" +} + +# --- OCIR --- +output "ocir_backend_url" { + description = "OCIR URL for the backend image" + value = "${var.region}.ocir.io/${var.ocir_namespace}/${var.ocir_repo_prefix}/backend" +} + +output "ocir_frontend_url" { + description = "OCIR URL for the frontend image" + value = "${var.region}.ocir.io/${var.ocir_namespace}/${var.ocir_repo_prefix}/frontend" +} + +output "ocir_docker_login_command" { + description = "Command to login to OCIR" + value = "docker login ${var.region}.ocir.io" +} + +# --- Networking --- +output "vcn_id" { + description = "OCID of the VCN" + value = oci_core_vcn.main.id +} + +output "nodes_subnet_id" { + description = "OCID of the nodes subnet" + value = oci_core_subnet.nodes.id +} + +# --- WAF --- +output "waf_id" { + description = "OCID of the WAF" + value = oci_waf_web_app_firewall.main.id +} + +# --- DNS (conditional) --- +output "dns_nameservers" { + description = "DNS Zone nameservers (set these at your domain registrar)" + value = var.domain_name != "" ? [for ns in oci_dns_zone.main[0].nameservers : ns.hostname] : [] +} diff --git a/deploy/terraform-oke/provider.tf b/deploy/terraform-oke/provider.tf new file mode 100644 index 0000000..9b877de --- /dev/null +++ b/deploy/terraform-oke/provider.tf @@ -0,0 +1,27 @@ +# ----------------------------------------------------------------------------- +# OCI CIS Agent — OKE Deployment Stack +# Provider Configuration +# ----------------------------------------------------------------------------- + +terraform { + required_version = ">= 1.5.0" + + required_providers { + oci = { + source = "oracle/oci" + version = ">= 6.0.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0.0" + } + } +} + +provider "oci" { + tenancy_ocid = var.tenancy_ocid + user_ocid = var.user_ocid + fingerprint = var.fingerprint + private_key_path = var.private_key_path + region = var.region +} diff --git a/deploy/terraform-oke/registry.tf b/deploy/terraform-oke/registry.tf new file mode 100644 index 0000000..005644f --- /dev/null +++ b/deploy/terraform-oke/registry.tf @@ -0,0 +1,18 @@ +# ----------------------------------------------------------------------------- +# OCI CIS Agent — OKE Deployment Stack +# OCIR: Container Image Repositories +# ----------------------------------------------------------------------------- + +resource "oci_artifacts_container_repository" "backend" { + compartment_id = var.compartment_ocid + display_name = "${var.ocir_repo_prefix}/backend" + is_public = false + is_immutable = false +} + +resource "oci_artifacts_container_repository" "frontend" { + compartment_id = var.compartment_ocid + display_name = "${var.ocir_repo_prefix}/frontend" + is_public = false + is_immutable = false +} diff --git a/deploy/terraform-oke/terraform.tfvars.example b/deploy/terraform-oke/terraform.tfvars.example new file mode 100644 index 0000000..af8c7a8 --- /dev/null +++ b/deploy/terraform-oke/terraform.tfvars.example @@ -0,0 +1,58 @@ +# ============================================================================= +# OCI CIS Agent — OKE Deployment Stack +# Copy this file to terraform.tfvars and fill in your values. +# ============================================================================= + +# --- OCI Authentication --- +tenancy_ocid = "ocid1.tenancy.oc1..aaaaaaaaXXXXXXXX" +user_ocid = "ocid1.user.oc1..aaaaaaaaXXXXXXXX" +fingerprint = "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99" +private_key_path = "~/.oci/oci_api_key.pem" +region = "us-ashburn-1" + +# --- Compartment --- +compartment_ocid = "ocid1.compartment.oc1..aaaaaaaaXXXXXXXX" + +# --- Networking (defaults are fine for most deployments) --- +# vcn_cidr = "10.0.0.0/16" +# public_subnet_cidr = "10.0.1.0/24" +# nodes_subnet_cidr = "10.0.2.0/24" +# api_subnet_cidr = "10.0.3.0/24" + +# --- OKE Cluster --- +# kubernetes_version = "v1.30.1" +# cluster_name = "oci-cis-agent-oke" + +# --- Node Pool --- +# node_shape = "VM.Standard3.Flex" +# node_ocpus = 2 +# node_memory_gb = 16 +# node_count = 3 +# node_boot_volume_gb = 50 + +# --- SSH Key --- +ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAA... your-key-here" + +# --- Application --- +# Generate with: python3 -c "import secrets; print(secrets.token_hex(64))" +app_secret = "YOUR_128_CHAR_HEX_SECRET_HERE" +# jwt_expiry_hours = 12 +# app_timezone = "UTC" + +# --- OCIR --- +ocir_namespace = "your-tenancy-namespace" +# ocir_repo_prefix = "oci-cis-agent" + +# --- Load Balancer --- +# lb_min_bandwidth_mbps = 10 +# lb_max_bandwidth_mbps = 100 +# node_port = 30080 + +# --- DNS (optional: leave empty to skip) --- +# domain_name = "cisagent.example.com" + +# --- Tags --- +# freeform_tags = { +# "Project" = "oci-cis-agent" +# "ManagedBy" = "terraform" +# } diff --git a/deploy/terraform-oke/variables.tf b/deploy/terraform-oke/variables.tf new file mode 100644 index 0000000..fcc6362 --- /dev/null +++ b/deploy/terraform-oke/variables.tf @@ -0,0 +1,198 @@ +# ----------------------------------------------------------------------------- +# OCI Authentication +# ----------------------------------------------------------------------------- +variable "tenancy_ocid" { + description = "OCID of the OCI tenancy" + type = string +} + +variable "user_ocid" { + description = "OCID of the OCI user for API authentication" + type = string +} + +variable "fingerprint" { + description = "Fingerprint of the OCI API signing key" + type = string +} + +variable "private_key_path" { + description = "Path to the OCI API private key PEM file" + type = string +} + +variable "region" { + description = "OCI region for deployment" + type = string + default = "us-ashburn-1" +} + +# ----------------------------------------------------------------------------- +# Compartment +# ----------------------------------------------------------------------------- +variable "compartment_ocid" { + description = "OCID of the existing compartment for all resources" + type = string +} + +# ----------------------------------------------------------------------------- +# Networking +# ----------------------------------------------------------------------------- +variable "vcn_cidr" { + description = "CIDR block for the VCN" + type = string + default = "10.0.0.0/16" +} + +variable "public_subnet_cidr" { + description = "CIDR block for the public subnet (Load Balancer)" + type = string + default = "10.0.1.0/24" +} + +variable "nodes_subnet_cidr" { + description = "CIDR block for the private subnet (OKE worker nodes)" + type = string + default = "10.0.2.0/24" +} + +variable "api_subnet_cidr" { + description = "CIDR block for the private subnet (OKE API endpoint)" + type = string + default = "10.0.3.0/24" +} + +# ----------------------------------------------------------------------------- +# OKE Cluster +# ----------------------------------------------------------------------------- +variable "kubernetes_version" { + description = "Kubernetes version for the OKE cluster" + type = string + default = "v1.30.1" +} + +variable "cluster_name" { + description = "Name of the OKE cluster" + type = string + default = "oci-cis-agent-oke" +} + +# ----------------------------------------------------------------------------- +# Node Pool +# ----------------------------------------------------------------------------- +variable "node_shape" { + description = "Shape of the OKE worker nodes" + type = string + default = "VM.Standard3.Flex" +} + +variable "node_ocpus" { + description = "Number of OCPUs per worker node" + type = number + default = 2 +} + +variable "node_memory_gb" { + description = "Memory in GB per worker node" + type = number + default = 16 +} + +variable "node_count" { + description = "Number of worker nodes in the node pool" + type = number + default = 3 +} + +variable "node_boot_volume_gb" { + description = "Boot volume size in GB per worker node" + type = number + default = 50 +} + +variable "ssh_public_key" { + description = "SSH public key for worker node access" + type = string +} + +# ----------------------------------------------------------------------------- +# Application Configuration +# ----------------------------------------------------------------------------- +variable "app_secret" { + description = "Application secret key (64-byte hex string, 128 characters)" + type = string + sensitive = true + + validation { + condition = can(regex("^[0-9a-fA-F]{128}$", var.app_secret)) + error_message = "app_secret must be a 128-character hexadecimal string (64 bytes)." + } +} + +variable "jwt_expiry_hours" { + description = "JWT token expiry time in hours" + type = number + default = 12 +} + +variable "app_timezone" { + description = "Timezone for the application (e.g. America/Sao_Paulo)" + type = string + default = "UTC" +} + +# ----------------------------------------------------------------------------- +# OCIR (Oracle Cloud Infrastructure Registry) +# ----------------------------------------------------------------------------- +variable "ocir_namespace" { + description = "Object Storage namespace for OCIR (tenancy namespace)" + type = string +} + +variable "ocir_repo_prefix" { + description = "Repository prefix in OCIR (e.g. oci-cis-agent)" + type = string + default = "oci-cis-agent" +} + +# ----------------------------------------------------------------------------- +# Load Balancer +# ----------------------------------------------------------------------------- +variable "lb_min_bandwidth_mbps" { + description = "Minimum bandwidth for the flexible load balancer in Mbps" + type = number + default = 10 +} + +variable "lb_max_bandwidth_mbps" { + description = "Maximum bandwidth for the flexible load balancer in Mbps" + type = number + default = 100 +} + +variable "node_port" { + description = "NodePort for the Kubernetes frontend service (must match K8s Service spec)" + type = number + default = 30080 +} + +# ----------------------------------------------------------------------------- +# DNS (Optional) +# ----------------------------------------------------------------------------- +variable "domain_name" { + description = "Domain name for DNS zone and A record. Leave empty to skip DNS setup." + type = string + default = "" +} + +# ----------------------------------------------------------------------------- +# Tags +# ----------------------------------------------------------------------------- +variable "freeform_tags" { + description = "Freeform tags to apply to all resources" + type = map(string) + default = { + "Project" = "oci-cis-agent" + "ManagedBy" = "terraform" + } +} diff --git a/deploy/terraform-oke/waf.tf b/deploy/terraform-oke/waf.tf new file mode 100644 index 0000000..ec10121 --- /dev/null +++ b/deploy/terraform-oke/waf.tf @@ -0,0 +1,114 @@ +# ----------------------------------------------------------------------------- +# OCI CIS Agent — OKE Deployment Stack +# WAF: Web Application Firewall Policy + Association +# ----------------------------------------------------------------------------- + +resource "oci_waf_web_app_firewall_policy" "main" { + compartment_id = var.compartment_ocid + display_name = "cisagent-oke-waf-policy" + + # --- OWASP Protection Rules --- + actions { + name = "block" + type = "RETURN_HTTP_RESPONSE" + code = 403 + body { + type = "STATIC_TEXT" + text = "Blocked by WAF" + } + } + + actions { + name = "allow" + type = "ALLOW" + } + + # --- Request Protection: XSS --- + request_protection { + rules { + name = "xss-protection" + type = "PROTECTION" + action_name = "block" + is_body_inspection_enabled = true + + protection_capabilities { + key = "941110" + version = 1 + } + protection_capabilities { + key = "941100" + version = 1 + } + protection_capabilities { + key = "941160" + version = 1 + } + } + + # --- Request Protection: SQL Injection --- + rules { + name = "sqli-protection" + type = "PROTECTION" + action_name = "block" + is_body_inspection_enabled = true + + protection_capabilities { + key = "942110" + version = 1 + } + protection_capabilities { + key = "942120" + version = 1 + } + protection_capabilities { + key = "942130" + version = 1 + } + } + + # --- Request Protection: Path Traversal --- + rules { + name = "path-traversal-protection" + type = "PROTECTION" + action_name = "block" + is_body_inspection_enabled = true + + protection_capabilities { + key = "930100" + version = 1 + } + protection_capabilities { + key = "930110" + version = 1 + } + } + } + + # --- Rate Limiting: 300 requests/minute --- + request_rate_limiting { + rules { + name = "rate-limit-300-per-minute" + type = "RATE_LIMITING" + action_name = "block" + + configurations { + period_in_seconds = 60 + requests_limit = 300 + action_duration_in_seconds = 60 + } + } + } + + freeform_tags = var.freeform_tags +} + +# --- Associate WAF with Load Balancer --- + +resource "oci_waf_web_app_firewall" "main" { + compartment_id = var.compartment_ocid + display_name = "cisagent-oke-waf" + backend_type = "LOAD_BALANCER" + load_balancer_id = oci_load_balancer_load_balancer.main.id + web_app_firewall_policy_id = oci_waf_web_app_firewall_policy.main.id + freeform_tags = var.freeform_tags +} diff --git a/deploy/terraform-vm/compute.tf b/deploy/terraform-vm/compute.tf new file mode 100644 index 0000000..fb12833 --- /dev/null +++ b/deploy/terraform-vm/compute.tf @@ -0,0 +1,78 @@ +# ----------------------------------------------------------------------------- +# Data Sources +# ----------------------------------------------------------------------------- +data "oci_identity_availability_domains" "ads" { + compartment_id = var.tenancy_ocid +} + +data "oci_core_images" "oracle_linux_8" { + compartment_id = var.compartment_ocid + operating_system = "Oracle Linux" + operating_system_version = "8" + shape = var.instance_shape + sort_by = "TIMECREATED" + sort_order = "DESC" + + filter { + name = "display_name" + values = ["^Oracle-Linux-8\\..*-aarch64-.*$"] + regex = true + } +} + +# ----------------------------------------------------------------------------- +# Compute Instance +# ----------------------------------------------------------------------------- +resource "oci_core_instance" "app" { + compartment_id = var.compartment_ocid + availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name + display_name = "cisagent-vm" + shape = var.instance_shape + freeform_tags = var.freeform_tags + + shape_config { + ocpus = var.instance_ocpus + memory_in_gbs = var.instance_memory_gb + } + + source_details { + source_type = "image" + source_id = data.oci_core_images.oracle_linux_8.images[0].id + } + + create_vnic_details { + subnet_id = oci_core_subnet.private.id + assign_public_ip = false + display_name = "cisagent-vnic" + hostname_label = "cisagent" + } + + metadata = { + ssh_authorized_keys = var.ssh_public_key + user_data = base64encode(templatefile("${path.module}/templates/cloud-init.sh.tpl", { + ocir_region = var.region + ocir_namespace = var.ocir_namespace + ocir_username = var.ocir_username + ocir_auth_token = var.ocir_auth_token + ocir_repo_prefix = var.ocir_repo_prefix + app_secret = var.app_secret + jwt_expiry_hours = var.jwt_expiry_hours + app_timezone = var.app_timezone + cors_origins = var.domain_name != "" ? "https://${var.domain_name}" : "*" + data_device = "/dev/oracleoci/oraclevdb" + })) + } + + agent_config { + is_monitoring_disabled = false + is_management_disabled = false + are_all_plugins_disabled = false + } + + lifecycle { + ignore_changes = [ + source_details[0].source_id, + metadata["user_data"], + ] + } +} diff --git a/deploy/terraform-vm/dns.tf b/deploy/terraform-vm/dns.tf new file mode 100644 index 0000000..cef7882 --- /dev/null +++ b/deploy/terraform-vm/dns.tf @@ -0,0 +1,36 @@ +# ----------------------------------------------------------------------------- +# DNS Zone and A Record (conditional — only when domain_name is set) +# ----------------------------------------------------------------------------- + +resource "oci_dns_zone" "main" { + count = var.domain_name != "" ? 1 : 0 + compartment_id = var.compartment_ocid + name = var.domain_name + zone_type = "PRIMARY" + freeform_tags = var.freeform_tags +} + +resource "oci_dns_rrset" "a_record" { + count = var.domain_name != "" ? 1 : 0 + zone_name_or_id = oci_dns_zone.main[0].id + domain = var.domain_name + rtype = "A" + compartment_id = var.compartment_ocid + + items { + domain = var.domain_name + rtype = "A" + rdata = oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address + ttl = 300 + } +} + +# TODO: Add OCI Certificates Service integration for Let's Encrypt when domain +# is validated. This requires: +# 1. oci_certificates_management_certificate_authority (subordinate CA) +# 2. oci_certificates_management_certificate with ISSUED_BY_INTERNAL_CA config +# 3. DNS-01 challenge validation via the OCI DNS zone above +# 4. Update LB listener SSL config to use the managed certificate +# For now, the self-signed cert is used even when a domain is configured. +# After DNS propagation, you can manually configure Let's Encrypt via certbot +# or the OCI Certificates Service console. diff --git a/deploy/terraform-vm/lb.tf b/deploy/terraform-vm/lb.tf new file mode 100644 index 0000000..8202b78 --- /dev/null +++ b/deploy/terraform-vm/lb.tf @@ -0,0 +1,143 @@ +# ----------------------------------------------------------------------------- +# Self-Signed TLS Certificate (fallback when no domain) +# ----------------------------------------------------------------------------- +resource "tls_private_key" "lb" { + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "lb" { + private_key_pem = tls_private_key.lb.private_key_pem + + subject { + common_name = var.domain_name != "" ? var.domain_name : "cisagent.local" + organization = "OCI CIS Agent" + } + + validity_period_hours = 8760 # 1 year + + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth", + ] +} + +# ----------------------------------------------------------------------------- +# Load Balancer +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_load_balancer" "main" { + compartment_id = var.compartment_ocid + display_name = "cisagent-lb" + shape = "flexible" + freeform_tags = var.freeform_tags + + shape_details { + minimum_bandwidth_in_mbps = var.lb_min_bandwidth_mbps + maximum_bandwidth_in_mbps = var.lb_max_bandwidth_mbps + } + + subnet_ids = [oci_core_subnet.public.id] + + is_private = false +} + +# ----------------------------------------------------------------------------- +# Certificate +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_certificate" "self_signed" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + certificate_name = "cisagent-self-signed" + public_certificate = tls_self_signed_cert.lb.cert_pem + private_key = tls_private_key.lb.private_key_pem + + lifecycle { + create_before_destroy = true + } +} + +# ----------------------------------------------------------------------------- +# Backend Set +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_backend_set" "app" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + name = "cisagent-backend-set" + policy = "ROUND_ROBIN" + + health_checker { + protocol = "HTTP" + port = 8080 + url_path = "/api/health" + interval_ms = 10000 + timeout_in_millis = 5000 + retries = 3 + return_code = 200 + } +} + +# ----------------------------------------------------------------------------- +# Backend +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_backend" "app" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + backendset_name = oci_load_balancer_backend_set.app.name + ip_address = oci_core_instance.app.private_ip + port = 8080 +} + +# ----------------------------------------------------------------------------- +# HTTPS Listener (443) +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_listener" "https" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + name = "cisagent-https-listener" + default_backend_set_name = oci_load_balancer_backend_set.app.name + port = 443 + protocol = "HTTP" + + ssl_configuration { + certificate_name = oci_load_balancer_certificate.self_signed.certificate_name + verify_peer_certificate = false + protocols = ["TLSv1.2", "TLSv1.3"] + } +} + +# ----------------------------------------------------------------------------- +# HTTP → HTTPS Redirect Rule Set +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_rule_set" "redirect_http_to_https" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + name = "redirect-http-to-https" + + items { + action = "REDIRECT" + + conditions { + attribute_name = "PATH" + attribute_value = "/" + operator = "FORCE_LONGEST_PREFIX_MATCH" + } + + redirect_uri { + protocol = "HTTPS" + port = 443 + host = "{host}" + path = "/{path}" + query = "?{query}" + } + + response_code = 301 + } +} + +# ----------------------------------------------------------------------------- +# HTTP Listener (80) with Redirect +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_listener" "http" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + name = "cisagent-http-listener" + default_backend_set_name = oci_load_balancer_backend_set.app.name + port = 80 + protocol = "HTTP" + rule_set_names = [oci_load_balancer_rule_set.redirect_http_to_https.name] +} diff --git a/deploy/terraform-vm/network.tf b/deploy/terraform-vm/network.tf new file mode 100644 index 0000000..5ceb520 --- /dev/null +++ b/deploy/terraform-vm/network.tf @@ -0,0 +1,189 @@ +# ----------------------------------------------------------------------------- +# VCN +# ----------------------------------------------------------------------------- +resource "oci_core_vcn" "main" { + compartment_id = var.compartment_ocid + cidr_blocks = [var.vcn_cidr] + display_name = "cisagent-vcn" + dns_label = "cisagent" + freeform_tags = var.freeform_tags +} + +# ----------------------------------------------------------------------------- +# Gateways +# ----------------------------------------------------------------------------- +resource "oci_core_internet_gateway" "main" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-igw" + enabled = true + freeform_tags = var.freeform_tags +} + +resource "oci_core_nat_gateway" "main" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-natgw" + freeform_tags = var.freeform_tags +} + +data "oci_core_services" "all" { + filter { + name = "name" + values = ["All .* Services In Oracle Services Network"] + regex = true + } +} + +resource "oci_core_service_gateway" "main" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-svcgw" + + services { + service_id = data.oci_core_services.all.services[0].id + } + + freeform_tags = var.freeform_tags +} + +# ----------------------------------------------------------------------------- +# Route Tables +# ----------------------------------------------------------------------------- +resource "oci_core_route_table" "public" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-public-rt" + freeform_tags = var.freeform_tags + + route_rules { + network_entity_id = oci_core_internet_gateway.main.id + destination = "0.0.0.0/0" + destination_type = "CIDR_BLOCK" + } +} + +resource "oci_core_route_table" "private" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-private-rt" + freeform_tags = var.freeform_tags + + route_rules { + network_entity_id = oci_core_nat_gateway.main.id + destination = "0.0.0.0/0" + destination_type = "CIDR_BLOCK" + } + + route_rules { + network_entity_id = oci_core_service_gateway.main.id + destination = data.oci_core_services.all.services[0].cidr_block + destination_type = "SERVICE_CIDR_BLOCK" + } +} + +# ----------------------------------------------------------------------------- +# Security Lists +# ----------------------------------------------------------------------------- +resource "oci_core_security_list" "public" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-public-sl" + freeform_tags = var.freeform_tags + + # Ingress: HTTP + ingress_security_rules { + protocol = "6" # TCP + source = "0.0.0.0/0" + stateless = false + + tcp_options { + min = 80 + max = 80 + } + } + + # Ingress: HTTPS + ingress_security_rules { + protocol = "6" + source = "0.0.0.0/0" + stateless = false + + tcp_options { + min = 443 + max = 443 + } + } + + # Egress: All + egress_security_rules { + protocol = "all" + destination = "0.0.0.0/0" + stateless = false + } +} + +resource "oci_core_security_list" "private" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-private-sl" + freeform_tags = var.freeform_tags + + # Ingress: LB health checks and traffic from public subnet + ingress_security_rules { + protocol = "6" + source = var.public_subnet_cidr + stateless = false + + tcp_options { + min = 8080 + max = 8080 + } + } + + # Ingress: SSH from within VCN + ingress_security_rules { + protocol = "6" + source = var.vcn_cidr + stateless = false + + tcp_options { + min = 22 + max = 22 + } + } + + # Egress: All + egress_security_rules { + protocol = "all" + destination = "0.0.0.0/0" + stateless = false + } +} + +# ----------------------------------------------------------------------------- +# Subnets +# ----------------------------------------------------------------------------- +resource "oci_core_subnet" "public" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + cidr_block = var.public_subnet_cidr + display_name = "cisagent-public-subnet" + dns_label = "pub" + route_table_id = oci_core_route_table.public.id + security_list_ids = [oci_core_security_list.public.id] + prohibit_public_ip_on_vnic = false + freeform_tags = var.freeform_tags +} + +resource "oci_core_subnet" "private" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + cidr_block = var.private_subnet_cidr + display_name = "cisagent-private-subnet" + dns_label = "priv" + route_table_id = oci_core_route_table.private.id + security_list_ids = [oci_core_security_list.private.id] + prohibit_public_ip_on_vnic = true + freeform_tags = var.freeform_tags +} diff --git a/deploy/terraform-vm/outputs.tf b/deploy/terraform-vm/outputs.tf new file mode 100644 index 0000000..70d7aa3 --- /dev/null +++ b/deploy/terraform-vm/outputs.tf @@ -0,0 +1,56 @@ +# ----------------------------------------------------------------------------- +# Outputs +# ----------------------------------------------------------------------------- + +output "load_balancer_ip" { + description = "Public IP address of the load balancer" + value = oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address +} + +output "app_url" { + description = "Application URL" + value = var.domain_name != "" ? "https://${var.domain_name}" : "https://${oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address}" +} + +output "instance_private_ip" { + description = "Private IP of the compute instance" + value = oci_core_instance.app.private_ip +} + +output "instance_ocid" { + description = "OCID of the compute instance" + value = oci_core_instance.app.id +} + +output "ocir_backend_url" { + description = "OCIR URL for the backend image" + value = "${local.ocir_endpoint}/${var.ocir_namespace}/${var.ocir_repo_prefix}/backend" +} + +output "ocir_frontend_url" { + description = "OCIR URL for the frontend image" + value = "${local.ocir_endpoint}/${var.ocir_namespace}/${var.ocir_repo_prefix}/frontend" +} + +output "ssh_command" { + description = "SSH command to connect to the instance (requires bastion or VPN)" + value = "ssh opc@${oci_core_instance.app.private_ip}" +} + +output "docker_login_command" { + description = "Docker login command for OCIR" + value = "docker login ${local.ocir_endpoint} -u '${var.ocir_namespace}/${var.ocir_username}'" + sensitive = false +} + +output "dns_nameservers" { + description = "DNS nameservers for the zone (only when domain_name is set)" + value = var.domain_name != "" ? oci_dns_zone.main[0].nameservers[*].hostname : [] +} + +# ----------------------------------------------------------------------------- +# Locals +# ----------------------------------------------------------------------------- +locals { + ocir_endpoint = "${var.region}.ocir.io" +} diff --git a/deploy/terraform-vm/provider.tf b/deploy/terraform-vm/provider.tf new file mode 100644 index 0000000..0b8d9ba --- /dev/null +++ b/deploy/terraform-vm/provider.tf @@ -0,0 +1,22 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + oci = { + source = "oracle/oci" + version = ">= 6.0.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0.0" + } + } +} + +provider "oci" { + tenancy_ocid = var.tenancy_ocid + user_ocid = var.user_ocid + fingerprint = var.fingerprint + private_key_path = var.private_key_path + region = var.region +} diff --git a/deploy/terraform-vm/registry.tf b/deploy/terraform-vm/registry.tf new file mode 100644 index 0000000..b9bf1ad --- /dev/null +++ b/deploy/terraform-vm/registry.tf @@ -0,0 +1,14 @@ +# ----------------------------------------------------------------------------- +# OCIR Repositories +# ----------------------------------------------------------------------------- +resource "oci_artifacts_container_repository" "backend" { + compartment_id = var.compartment_ocid + display_name = "${var.ocir_repo_prefix}/backend" + is_public = false +} + +resource "oci_artifacts_container_repository" "frontend" { + compartment_id = var.compartment_ocid + display_name = "${var.ocir_repo_prefix}/frontend" + is_public = false +} diff --git a/deploy/terraform-vm/storage.tf b/deploy/terraform-vm/storage.tf new file mode 100644 index 0000000..f9ac9d9 --- /dev/null +++ b/deploy/terraform-vm/storage.tf @@ -0,0 +1,22 @@ +# ----------------------------------------------------------------------------- +# Block Volume for /data persistence +# ----------------------------------------------------------------------------- +resource "oci_core_volume" "data" { + compartment_id = var.compartment_ocid + availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name + display_name = "cisagent-data-vol" + size_in_gbs = var.block_volume_size_gb + vpus_per_gb = var.block_volume_vpus_per_gb + freeform_tags = var.freeform_tags +} + +resource "oci_core_volume_attachment" "data" { + attachment_type = "paravirtualized" + instance_id = oci_core_instance.app.id + volume_id = oci_core_volume.data.id + display_name = "cisagent-data-vol-attach" + device = "/dev/oracleoci/oraclevdb" + + # Ensure instance is fully running before attaching + depends_on = [oci_core_instance.app] +} diff --git a/deploy/terraform-vm/templates/cloud-init.sh.tpl b/deploy/terraform-vm/templates/cloud-init.sh.tpl new file mode 100644 index 0000000..90e8afd --- /dev/null +++ b/deploy/terraform-vm/templates/cloud-init.sh.tpl @@ -0,0 +1,163 @@ +#!/bin/bash +set -euo pipefail + +exec > >(tee /var/log/cloud-init-app.log) 2>&1 +echo "=== OCI CIS Agent cloud-init started at $(date) ===" + +# ----------------------------------------------------------------------------- +# 1. Install Docker CE + docker-compose-plugin +# ----------------------------------------------------------------------------- +echo ">>> Installing Docker CE..." +dnf install -y dnf-utils +dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin --allowerasing + +systemctl enable docker +systemctl start docker + +echo ">>> Docker version: $(docker --version)" +echo ">>> Docker Compose version: $(docker compose version)" + +# ----------------------------------------------------------------------------- +# 2. Wait for block device +# ----------------------------------------------------------------------------- +DATA_DEVICE="${data_device}" +echo ">>> Waiting for block device $DATA_DEVICE..." +ATTEMPTS=0 +MAX_ATTEMPTS=60 +while [ ! -e "$DATA_DEVICE" ] && [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; do + echo " Waiting for $DATA_DEVICE (attempt $((ATTEMPTS+1))/$MAX_ATTEMPTS)..." + sleep 5 + ATTEMPTS=$((ATTEMPTS+1)) +done + +if [ ! -e "$DATA_DEVICE" ]; then + echo "ERROR: Block device $DATA_DEVICE not found after $MAX_ATTEMPTS attempts" + exit 1 +fi +echo ">>> Block device $DATA_DEVICE is available" + +# ----------------------------------------------------------------------------- +# 3. Format and mount /data +# ----------------------------------------------------------------------------- +if ! blkid "$DATA_DEVICE" | grep -q ext4; then + echo ">>> Formatting $DATA_DEVICE as ext4..." + mkfs.ext4 -F "$DATA_DEVICE" +fi + +mkdir -p /data +mount "$DATA_DEVICE" /data + +# Add to fstab if not already present +if ! grep -q "$DATA_DEVICE" /etc/fstab; then + echo "$DATA_DEVICE /data ext4 defaults,_netdev,nofail 0 2" >> /etc/fstab +fi + +echo ">>> /data mounted successfully" +df -h /data + +# Create application data directories +mkdir -p /data/db /data/configs /data/reports /data/terraform /data/wallets + +# ----------------------------------------------------------------------------- +# 4. Docker login to OCIR +# ----------------------------------------------------------------------------- +OCIR_ENDPOINT="${ocir_region}.ocir.io" +echo ">>> Logging into OCIR at $OCIR_ENDPOINT..." +echo '${ocir_auth_token}' | docker login "$OCIR_ENDPOINT" \ + -u "${ocir_namespace}/${ocir_username}" \ + --password-stdin + +# ----------------------------------------------------------------------------- +# 5. Pull images +# ----------------------------------------------------------------------------- +BACKEND_IMAGE="$OCIR_ENDPOINT/${ocir_namespace}/${ocir_repo_prefix}/backend:latest" +FRONTEND_IMAGE="$OCIR_ENDPOINT/${ocir_namespace}/${ocir_repo_prefix}/frontend:latest" + +echo ">>> Pulling backend image: $BACKEND_IMAGE" +docker pull "$BACKEND_IMAGE" + +echo ">>> Pulling frontend image: $FRONTEND_IMAGE" +docker pull "$FRONTEND_IMAGE" + +# ----------------------------------------------------------------------------- +# 6. Write docker-compose.yml +# ----------------------------------------------------------------------------- +APP_DIR="/opt/oci-cis-agent" +mkdir -p "$APP_DIR" + +cat > "$APP_DIR/docker-compose.yml" <<'COMPOSE_EOF' +services: + backend: + image: ${ocir_region}.ocir.io/${ocir_namespace}/${ocir_repo_prefix}/backend:latest + container_name: cisagent-backend + restart: unless-stopped + environment: + - APP_SECRET=${app_secret} + - JWT_EXPIRY_HOURS=${jwt_expiry_hours} + - CORS_ORIGINS=${cors_origins} + - TZ=${app_timezone} + volumes: + - /data:/data + mem_limit: 4g + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + networks: + - agent-net + + frontend: + image: ${ocir_region}.ocir.io/${ocir_namespace}/${ocir_repo_prefix}/frontend:latest + container_name: cisagent-frontend + restart: unless-stopped + ports: + - "8080:80" + mem_limit: 512m + depends_on: + backend: + condition: service_healthy + networks: + - agent-net + +networks: + agent-net: + driver: bridge +COMPOSE_EOF + +echo ">>> docker-compose.yml written to $APP_DIR" + +# ----------------------------------------------------------------------------- +# 7. Start application +# ----------------------------------------------------------------------------- +echo ">>> Starting application with Docker Compose..." +cd "$APP_DIR" +docker compose up -d + +# ----------------------------------------------------------------------------- +# 8. Health check wait loop +# ----------------------------------------------------------------------------- +echo ">>> Waiting for application to become healthy..." +ATTEMPTS=0 +MAX_ATTEMPTS=60 +while [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; do + if curl -sf http://localhost:8080/api/health > /dev/null 2>&1; then + echo ">>> Application is healthy!" + break + fi + echo " Health check attempt $((ATTEMPTS+1))/$MAX_ATTEMPTS..." + sleep 10 + ATTEMPTS=$((ATTEMPTS+1)) +done + +if [ $ATTEMPTS -eq $MAX_ATTEMPTS ]; then + echo "WARNING: Application did not become healthy within expected time" + echo ">>> Docker container status:" + docker compose ps + echo ">>> Backend logs:" + docker compose logs backend --tail 50 +fi + +echo "=== OCI CIS Agent cloud-init finished at $(date) ===" diff --git a/deploy/terraform-vm/terraform.tfvars.example b/deploy/terraform-vm/terraform.tfvars.example new file mode 100644 index 0000000..40f41e2 --- /dev/null +++ b/deploy/terraform-vm/terraform.tfvars.example @@ -0,0 +1,55 @@ +# ============================================================================= +# OCI CIS Agent — VM Deployment Variables +# ============================================================================= +# Copy this file to terraform.tfvars and fill in the values. +# Required values are marked with . +# ============================================================================= + +# --- OCI Authentication --- +tenancy_ocid = "" +user_ocid = "" +fingerprint = "" +private_key_path = "" # e.g. "~/.oci/oci_api_key.pem" +region = "us-ashburn-1" + +# --- Compartment --- +compartment_ocid = "" + +# --- Networking (defaults are usually fine) --- +# vcn_cidr = "10.0.0.0/16" +# public_subnet_cidr = "10.0.1.0/24" +# private_subnet_cidr = "10.0.2.0/24" + +# --- Compute Instance --- +# instance_shape = "VM.Standard.A1.Flex" +# instance_ocpus = 2 +# instance_memory_gb = 16 +ssh_public_key = "" # contents of ~/.ssh/id_rsa.pub + +# --- Block Volume --- +# block_volume_size_gb = 50 +# block_volume_vpus_per_gb = 20 + +# --- Application --- +app_secret = "" # generate with: python3 -c "import secrets; print(secrets.token_hex(64))" +# jwt_expiry_hours = 12 +# app_timezone = "UTC" + +# --- OCIR --- +ocir_namespace = "" # tenancy Object Storage namespace +ocir_username = "" # e.g. "user@email.com" (without namespace prefix) +ocir_auth_token = "" # generate in OCI Console > User Settings > Auth Tokens +# ocir_repo_prefix = "oci-cis-agent" + +# --- Load Balancer --- +# lb_min_bandwidth_mbps = 10 +# lb_max_bandwidth_mbps = 100 + +# --- DNS (optional — leave empty to skip) --- +# domain_name = "" + +# --- Tags --- +# freeform_tags = { +# "Project" = "oci-cis-agent" +# "ManagedBy" = "terraform" +# } diff --git a/deploy/terraform-vm/variables.tf b/deploy/terraform-vm/variables.tf new file mode 100644 index 0000000..86d911c --- /dev/null +++ b/deploy/terraform-vm/variables.tf @@ -0,0 +1,185 @@ +# ----------------------------------------------------------------------------- +# OCI Authentication +# ----------------------------------------------------------------------------- +variable "tenancy_ocid" { + description = "OCID of the OCI tenancy" + type = string +} + +variable "user_ocid" { + description = "OCID of the OCI user for API authentication" + type = string +} + +variable "fingerprint" { + description = "Fingerprint of the OCI API signing key" + type = string +} + +variable "private_key_path" { + description = "Path to the OCI API private key PEM file" + type = string +} + +variable "region" { + description = "OCI region for deployment" + type = string + default = "us-ashburn-1" +} + +# ----------------------------------------------------------------------------- +# Compartment +# ----------------------------------------------------------------------------- +variable "compartment_ocid" { + description = "OCID of the existing compartment for all resources" + type = string +} + +# ----------------------------------------------------------------------------- +# Networking +# ----------------------------------------------------------------------------- +variable "vcn_cidr" { + description = "CIDR block for the VCN" + type = string + default = "10.0.0.0/16" +} + +variable "public_subnet_cidr" { + description = "CIDR block for the public subnet (Load Balancer)" + type = string + default = "10.0.1.0/24" +} + +variable "private_subnet_cidr" { + description = "CIDR block for the private subnet (Compute)" + type = string + default = "10.0.2.0/24" +} + +# ----------------------------------------------------------------------------- +# Compute Instance +# ----------------------------------------------------------------------------- +variable "instance_shape" { + description = "Compute instance shape" + type = string + default = "VM.Standard.A1.Flex" +} + +variable "instance_ocpus" { + description = "Number of OCPUs for the flex instance" + type = number + default = 2 +} + +variable "instance_memory_gb" { + description = "Memory in GB for the flex instance" + type = number + default = 16 +} + +variable "ssh_public_key" { + description = "SSH public key for instance access" + type = string +} + +# ----------------------------------------------------------------------------- +# Block Volume +# ----------------------------------------------------------------------------- +variable "block_volume_size_gb" { + description = "Size of the block volume in GB for /data" + type = number + default = 50 +} + +variable "block_volume_vpus_per_gb" { + description = "Volume performance units per GB (10=Balanced, 20=Higher, 30+=Ultra High)" + type = number + default = 20 +} + +# ----------------------------------------------------------------------------- +# Application Configuration +# ----------------------------------------------------------------------------- +variable "app_secret" { + description = "Application secret key (64-byte hex string, 128 characters)" + type = string + sensitive = true + + validation { + condition = can(regex("^[0-9a-fA-F]{128}$", var.app_secret)) + error_message = "app_secret must be a 128-character hexadecimal string (64 bytes)." + } +} + +variable "jwt_expiry_hours" { + description = "JWT token expiry time in hours" + type = number + default = 12 +} + +variable "app_timezone" { + description = "Timezone for the application (e.g. America/Sao_Paulo)" + type = string + default = "UTC" +} + +# ----------------------------------------------------------------------------- +# OCIR (Oracle Cloud Infrastructure Registry) +# ----------------------------------------------------------------------------- +variable "ocir_namespace" { + description = "Object Storage namespace for OCIR (tenancy namespace)" + type = string +} + +variable "ocir_username" { + description = "Username for OCIR login (e.g. tenancy-namespace/user@email.com)" + type = string +} + +variable "ocir_auth_token" { + description = "Auth token for OCIR login" + type = string + sensitive = true +} + +variable "ocir_repo_prefix" { + description = "Repository prefix in OCIR (e.g. oci-cis-agent)" + type = string + default = "oci-cis-agent" +} + +# ----------------------------------------------------------------------------- +# Load Balancer +# ----------------------------------------------------------------------------- +variable "lb_min_bandwidth_mbps" { + description = "Minimum bandwidth for the flexible load balancer in Mbps" + type = number + default = 10 +} + +variable "lb_max_bandwidth_mbps" { + description = "Maximum bandwidth for the flexible load balancer in Mbps" + type = number + default = 100 +} + +# ----------------------------------------------------------------------------- +# DNS (Optional) +# ----------------------------------------------------------------------------- +variable "domain_name" { + description = "Domain name for DNS zone and A record. Leave empty to skip DNS setup." + type = string + default = "" +} + +# ----------------------------------------------------------------------------- +# Tags +# ----------------------------------------------------------------------------- +variable "freeform_tags" { + description = "Freeform tags to apply to all resources" + type = map(string) + default = { + "Project" = "oci-cis-agent" + "ManagedBy" = "terraform" + } +} diff --git a/deploy/terraform-vm/waf.tf b/deploy/terraform-vm/waf.tf new file mode 100644 index 0000000..c9d2d91 --- /dev/null +++ b/deploy/terraform-vm/waf.tf @@ -0,0 +1,120 @@ +# ----------------------------------------------------------------------------- +# WAF Policy +# ----------------------------------------------------------------------------- +resource "oci_waf_web_app_firewall_policy" "main" { + compartment_id = var.compartment_ocid + display_name = "cisagent-waf-policy" + freeform_tags = var.freeform_tags + + # --- Actions --- + actions { + name = "checkAction" + type = "CHECK" + } + + actions { + name = "allowAction" + type = "ALLOW" + } + + actions { + name = "blockAction" + type = "RETURN_HTTP_RESPONSE" + code = 401 + + headers { + name = "Content-Type" + value = "application/json" + } + + body { + type = "STATIC_TEXT" + text = "{\"error\": \"Request blocked by WAF\"}" + } + } + + # --- Request Protection --- + request_protection { + + # XSS Protection + rules { + name = "xss-protection" + type = "PROTECTION" + action_name = "blockAction" + is_body_inspection_enabled = true + + protection_capabilities { + key = "941110" + version = 1 + } + + protection_capabilities { + key = "941100" + version = 1 + } + } + + # SQL Injection Protection + rules { + name = "sqli-protection" + type = "PROTECTION" + action_name = "blockAction" + is_body_inspection_enabled = true + + protection_capabilities { + key = "942100" + version = 1 + } + + protection_capabilities { + key = "942110" + version = 1 + } + } + + # Path Traversal Protection + rules { + name = "path-traversal-protection" + type = "PROTECTION" + action_name = "blockAction" + is_body_inspection_enabled = true + + protection_capabilities { + key = "930100" + version = 1 + } + + protection_capabilities { + key = "930110" + version = 1 + } + } + } + + # --- Rate Limiting --- + request_rate_limiting { + rules { + name = "rate-limit-300-per-min" + type = "RATE_LIMITING" + action_name = "blockAction" + + configurations { + period_in_seconds = 60 + requests_limit = 300 + action_duration_in_seconds = 60 + } + } + } +} + +# ----------------------------------------------------------------------------- +# WAF Firewall (attached to Load Balancer) +# ----------------------------------------------------------------------------- +resource "oci_waf_web_app_firewall" "main" { + compartment_id = var.compartment_ocid + display_name = "cisagent-waf" + backend_type = "LOAD_BALANCER" + load_balancer_id = oci_load_balancer_load_balancer.main.id + web_app_firewall_policy_id = oci_waf_web_app_firewall_policy.main.id + freeform_tags = var.freeform_tags +}