diff --git a/.gitignore b/.gitignore index 9b57fbc..b24ff5c 100644 --- a/.gitignore +++ b/.gitignore @@ -39,5 +39,9 @@ dist/ !*.tfvars.example .terraform.lock.hcl +# Private deployment (OKE/K8s — not for public distribution) +deploy/terraform-oke/ +deploy/k8s/ + # Project internal docs LESSONS_LEARNED.md diff --git a/deploy/k8s/configmap.yaml b/deploy/k8s/configmap.yaml deleted file mode 100644 index f90b438..0000000 --- a/deploy/k8s/configmap.yaml +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index c7b4de6..0000000 --- a/deploy/k8s/deployment.yaml +++ /dev/null @@ -1,111 +0,0 @@ -## -## 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 deleted file mode 100644 index 415f463..0000000 --- a/deploy/k8s/namespace.yaml +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 18268ff..0000000 --- a/deploy/k8s/pvc.yaml +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index c0ce659..0000000 --- a/deploy/k8s/secret.yaml +++ /dev/null @@ -1,35 +0,0 @@ -## -## 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 deleted file mode 100644 index f4ab6ad..0000000 --- a/deploy/k8s/service.yaml +++ /dev/null @@ -1,42 +0,0 @@ -## -## 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/terraform-oke/dns.tf b/deploy/terraform-oke/dns.tf deleted file mode 100644 index 104d624..0000000 --- a/deploy/terraform-oke/dns.tf +++ /dev/null @@ -1,29 +0,0 @@ -# ----------------------------------------------------------------------------- -# 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/network.tf b/deploy/terraform-oke/network.tf deleted file mode 100644 index de61e4f..0000000 --- a/deploy/terraform-oke/network.tf +++ /dev/null @@ -1,287 +0,0 @@ -# ----------------------------------------------------------------------------- -# 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 deleted file mode 100644 index 408b784..0000000 --- a/deploy/terraform-oke/oke.tf +++ /dev/null @@ -1,116 +0,0 @@ -# ----------------------------------------------------------------------------- -# 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 deleted file mode 100644 index 20a9c58..0000000 --- a/deploy/terraform-oke/outputs.tf +++ /dev/null @@ -1,75 +0,0 @@ -# ----------------------------------------------------------------------------- -# 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/terraform.tfvars.example b/deploy/terraform-oke/terraform.tfvars.example deleted file mode 100644 index af8c7a8..0000000 --- a/deploy/terraform-oke/terraform.tfvars.example +++ /dev/null @@ -1,58 +0,0 @@ -# ============================================================================= -# 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/distribution/.env.example b/distribution/.env.example new file mode 100644 index 0000000..aa8cc1e --- /dev/null +++ b/distribution/.env.example @@ -0,0 +1,29 @@ +# ── REQUIRED ──────────────────────────────────────────────────────────────── + +# OCIR region (e.g., us-ashburn-1, sa-saopaulo-1) +OCIR_REGION=us-ashburn-1 + +# OCIR namespace (find in OCI Console > Developer Services > Container Registry) +OCIR_NAMESPACE= + +# Application secret — REQUIRED +# Generate with: openssl rand -hex 64 +# This key encrypts JWT tokens, credentials, and session data. +APP_SECRET= + +# ── OPTIONAL ──────────────────────────────────────────────────────────────── + +# Image tag (default: latest) +# IMAGE_TAG=latest + +# JWT token expiry in hours (default: 12) +# JWT_EXPIRY_HOURS=12 + +# Frontend port (default: 8080) +# PORT=8080 + +# CORS allowed origins (comma-separated) +# CORS_ORIGINS=https://your-domain.com + +# Timezone (default: America/Sao_Paulo) +# TZ=America/Sao_Paulo diff --git a/distribution/README.md b/distribution/README.md new file mode 100644 index 0000000..1289932 --- /dev/null +++ b/distribution/README.md @@ -0,0 +1,233 @@ +

+ AI Agent +

+ +

AI Agent — Infrastructure & Security Engineer

+ +

+ Oracle Cloud Infrastructure — CIS Foundations Benchmark 3.0 — AI-Powered Compliance Platform +

+ +

+ Version + OCI + Docker + Terraform +

+ +--- + +## Overview + +AI Agent is a self-hosted web application that automates **CIS Oracle Cloud Infrastructure Foundations Benchmark 3.0** compliance checks, powered by **OCI Generative AI** for intelligent analysis and **MCP (Model Context Protocol)** for extensible task execution. + +The platform runs as pre-built Docker containers — no source code required. + +--- + +## Features + +- **AI Chat Agent** with RAG (Retrieval-Augmented Generation) + MCP Tool Use +- **Terraform Agent** for AI-powered IaC generation (plan/apply/destroy) +- **OCI Account Explorer** — 36 resource types, KPI dashboard, start/stop actions +- **OCI CLI Terminal** — web terminal with Tab autocomplete, OCID auto-lookup +- **CIS Compliance Reports** — 48 CIS + 11 OBP checks, PDF/DOCX export +- **Compliance Report** — professional Oracle-format assessment with RAG remediation +- **OCI Services Status** — auto-detect security services + OCI Health (49 regions) +- **Embeddings & Knowledge Base** — CIS PDF chunker, ADB vector store +- **Oracle IAM OIDC** — SSO via Oracle Identity Domains +- **Security** — JWT + TOTP MFA + RBAC (3 roles) + Fernet encryption + WAF +- **i18n** — Portuguese, English, Spanish + +--- + +## Quick Start (Docker Compose) + +### Prerequisites + +- Docker and Docker Compose +- Access to the OCI Container Registry (OCIR) with pull credentials + +### 1. Configure + +```bash +cp .env.example .env +``` + +Edit `.env`: +```env +OCIR_REGION=us-ashburn-1 +OCIR_NAMESPACE=your_namespace +APP_SECRET= +``` + +### 2. Login to OCIR + +```bash +docker login ${OCIR_REGION}.ocir.io +``` + +Username: `/` (or `/oracleidentitycloudservice/`) +Password: Auth Token (generate in OCI Console > Profile > Auth Tokens) + +### 3. Run + +```bash +docker compose up -d +``` + +### 4. Access + +Open `http://localhost:8080` + +The initial admin password is generated automatically and displayed in the backend logs: + +```bash +docker compose logs backend | grep "password" +``` + +> You will be prompted to change the password on first login. + +--- + +## Deploy on OCI (Terraform) + +For production deployment on Oracle Cloud Infrastructure with Load Balancer, WAF, and SSL. + +### Architecture + +``` +Internet --> WAF --> Load Balancer (HTTPS 443) + | + Private Subnet + | + Compute Instance (ARM, Free Tier eligible) + +-- Backend container (FastAPI) + +-- Frontend container (nginx) + +-- Block Volume (/data) +``` + +### Setup + +```bash +cd terraform +cp terraform.tfvars.example terraform.tfvars +``` + +Edit `terraform.tfvars` with your OCI credentials and configuration. + +### Deploy + +```bash +terraform init +terraform plan +terraform apply +``` + +### Resources Created + +| Resource | Description | +|----------|-------------| +| VCN | Virtual Cloud Network with public/private subnets | +| Compute | VM.Standard.A1.Flex (ARM, Free Tier eligible) | +| Block Volume | 50GB persistent storage for /data | +| Load Balancer | Flexible 10-100 Mbps with SSL | +| WAF | OWASP protection (XSS, SQLi, path traversal) | +| OCIR | 2 private container repositories | +| DNS | Conditional — OCI DNS Zone + A record (when domain configured) | + +### Outputs + +After `terraform apply`: + +```bash +terraform output load_balancer_ip # Public IP +terraform output app_url # https:// +terraform output ocir_backend_url # OCIR image URL +``` + +--- + +## Configuration Guide + +### Step 1 — OCI Credentials + +Navigate to **OCI Credentials** and add your tenancy details: + +| Field | Description | +|-------|-------------| +| Tenancy Name | Friendly name | +| OCID Tenancy | `ocid1.tenancy.oc1..xxxxx` | +| OCID User | `ocid1.user.oc1..xxxxx` | +| Fingerprint | `aa:bb:cc:dd:...` | +| Region | `us-ashburn-1` | +| Private Key | `.pem` file | + +### Step 2 — GenAI Model + +Select an OCI credential, choose a model from the catalog, and adjust parameters. + +### Step 3 — ADB Vector (Optional) + +For RAG-powered chat, configure an Autonomous Database with vector tables. + +### Step 4 — MCP Servers (Optional) + +Register MCP servers for extended tool use in the Chat Agent. + +--- + +## OCI IAM Policies + +``` +Allow group to use generative-ai-family in compartment +Allow group to read all-resources in tenancy +Allow group to inspect compartments in tenancy +Allow group to inspect autonomous-databases in compartment +Allow group to read virtual-network-family in compartment +Allow group to read instance-family in compartment +Allow group to read objectstorage-namespaces in tenancy +Allow group to read buckets in compartment +``` + +--- + +## Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `OCIR_REGION` | Yes | — | OCI region (e.g., `us-ashburn-1`) | +| `OCIR_NAMESPACE` | Yes | — | OCIR namespace | +| `APP_SECRET` | Yes | — | 64-byte hex key for JWT/encryption | +| `JWT_EXPIRY_HOURS` | No | `12` | Token expiry | +| `PORT` | No | `8080` | Frontend port | +| `CORS_ORIGINS` | No | — | Allowed origins (comma-separated) | +| `TZ` | No | `America/Sao_Paulo` | Timezone | + +--- + +## Troubleshooting + +**Cannot pull images from OCIR:** +Verify your `docker login` credentials and that the OCIR repositories exist in your namespace. + +**Backend health check fails:** +Check logs: `docker compose logs backend`. Ensure `APP_SECRET` is set. + +**ADB connection fails:** +Ensure the wallet ZIP is uploaded and the DSN matches a service in `tnsnames.ora`. + +**GenAI returns 401/403:** +Verify the IAM policy for `generative-ai-family` exists for your user/group. + +--- + +## License + +MIT + +--- + +

+ Built for Oracle Cloud Infrastructure security compliance +

diff --git a/distribution/docker-compose.yml b/distribution/docker-compose.yml new file mode 100644 index 0000000..346e598 --- /dev/null +++ b/distribution/docker-compose.yml @@ -0,0 +1,54 @@ +services: + backend: + image: ${OCIR_REGION}.ocir.io/${OCIR_NAMESPACE}/oci-cis-agent-backend:${IMAGE_TAG:-latest} + container_name: oci-agent-backend + restart: unless-stopped + environment: + - APP_SECRET=${APP_SECRET:?Set APP_SECRET in .env (openssl rand -hex 64)} + - JWT_EXPIRY_HOURS=${JWT_EXPIRY_HOURS:-12} + - DATA_DIR=/data + - CORS_ORIGINS=${CORS_ORIGINS:-} + - TZ=${TZ:-America/Sao_Paulo} + - OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=True + volumes: + - agent-data:/data + networks: + - agent-net + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + mem_limit: 4g + + frontend: + image: ${OCIR_REGION}.ocir.io/${OCIR_NAMESPACE}/oci-cis-agent-frontend:${IMAGE_TAG:-latest} + container_name: oci-agent-frontend + restart: unless-stopped + environment: + - TZ=${TZ:-America/Sao_Paulo} + ports: + - "${PORT:-8080}:80" + depends_on: + - backend + networks: + - agent-net + logging: + driver: "json-file" + options: + max-size: "5m" + max-file: "3" + mem_limit: 512m + +volumes: + agent-data: + driver: local + +networks: + agent-net: + driver: bridge diff --git a/distribution/terraform/compute.tf b/distribution/terraform/compute.tf new file mode 100644 index 0000000..fb12833 --- /dev/null +++ b/distribution/terraform/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/distribution/terraform/dns.tf b/distribution/terraform/dns.tf new file mode 100644 index 0000000..cef7882 --- /dev/null +++ b/distribution/terraform/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-oke/lb.tf b/distribution/terraform/lb.tf similarity index 51% rename from deploy/terraform-oke/lb.tf rename to distribution/terraform/lb.tf index 5ab4811..8202b78 100644 --- a/deploy/terraform-oke/lb.tf +++ b/distribution/terraform/lb.tf @@ -1,10 +1,6 @@ # ----------------------------------------------------------------------------- -# OCI CIS Agent — OKE Deployment Stack -# Load Balancer: Self-Signed TLS, HTTPS/HTTP Listeners, Backend Set +# Self-Signed TLS Certificate (fallback when no domain) # ----------------------------------------------------------------------------- - -# --- Self-Signed TLS Certificate --- - resource "tls_private_key" "lb" { algorithm = "RSA" rsa_bits = 2048 @@ -14,12 +10,11 @@ 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" + common_name = var.domain_name != "" ? var.domain_name : "cisagent.local" organization = "OCI CIS Agent" } validity_period_hours = 8760 # 1 year - is_ca_certificate = false allowed_uses = [ "key_encipherment", @@ -28,12 +23,14 @@ resource "tls_self_signed_cert" "lb" { ] } -# --- Load Balancer --- - +# ----------------------------------------------------------------------------- +# Load Balancer +# ----------------------------------------------------------------------------- resource "oci_load_balancer_load_balancer" "main" { compartment_id = var.compartment_ocid - display_name = "cisagent-oke-lb" + display_name = "cisagent-lb" shape = "flexible" + freeform_tags = var.freeform_tags shape_details { minimum_bandwidth_in_mbps = var.lb_min_bandwidth_mbps @@ -43,13 +40,12 @@ resource "oci_load_balancer_load_balancer" "main" { subnet_ids = [oci_core_subnet.public.id] is_private = false - - freeform_tags = var.freeform_tags } -# --- Certificate --- - -resource "oci_load_balancer_certificate" "main" { +# ----------------------------------------------------------------------------- +# 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 @@ -60,83 +56,61 @@ resource "oci_load_balancer_certificate" "main" { } } -# --- Backend Set --- -# Backends point to worker node IPs on the fixed NodePort. -# The K8s Service must use spec.type=NodePort with nodePort=30080. - +# ----------------------------------------------------------------------------- +# 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 = var.node_port - url_path = "/api/health" - return_code = 200 - interval_ms = 10000 - timeout_in_millis = 5000 - retries = 3 + protocol = "HTTP" + port = 8080 + url_path = "/api/health" + interval_ms = 10000 + timeout_in_millis = 5000 + retries = 3 + return_code = 200 } } -# --- 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" - } - +# ----------------------------------------------------------------------------- +# 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 = each.value.private_ip - port = var.node_port - weight = 1 + ip_address = oci_core_instance.app.private_ip + port = 8080 } -# --- HTTPS Listener (443) --- - +# ----------------------------------------------------------------------------- +# HTTPS Listener (443) +# ----------------------------------------------------------------------------- resource "oci_load_balancer_listener" "https" { load_balancer_id = oci_load_balancer_load_balancer.main.id - name = "cisagent-https" + 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.main.certificate_name + certificate_name = oci_load_balancer_certificate.self_signed.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" { +# ----------------------------------------------------------------------------- +# 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 = "http-to-https-redirect" + name = "redirect-http-to-https" items { - action = "REDIRECT" - description = "Redirect HTTP to HTTPS" + action = "REDIRECT" conditions { attribute_name = "PATH" @@ -155,3 +129,15 @@ resource "oci_load_balancer_rule_set" "http_redirect" { 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/distribution/terraform/network.tf b/distribution/terraform/network.tf new file mode 100644 index 0000000..5ceb520 --- /dev/null +++ b/distribution/terraform/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/distribution/terraform/outputs.tf b/distribution/terraform/outputs.tf new file mode 100644 index 0000000..70d7aa3 --- /dev/null +++ b/distribution/terraform/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-oke/provider.tf b/distribution/terraform/provider.tf similarity index 65% rename from deploy/terraform-oke/provider.tf rename to distribution/terraform/provider.tf index 9b877de..0b8d9ba 100644 --- a/deploy/terraform-oke/provider.tf +++ b/distribution/terraform/provider.tf @@ -1,8 +1,3 @@ -# ----------------------------------------------------------------------------- -# OCI CIS Agent — OKE Deployment Stack -# Provider Configuration -# ----------------------------------------------------------------------------- - terraform { required_version = ">= 1.5.0" diff --git a/deploy/terraform-oke/registry.tf b/distribution/terraform/registry.tf similarity index 80% rename from deploy/terraform-oke/registry.tf rename to distribution/terraform/registry.tf index 005644f..b9bf1ad 100644 --- a/deploy/terraform-oke/registry.tf +++ b/distribution/terraform/registry.tf @@ -1,18 +1,14 @@ # ----------------------------------------------------------------------------- -# OCI CIS Agent — OKE Deployment Stack -# OCIR: Container Image Repositories +# OCIR 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/distribution/terraform/storage.tf b/distribution/terraform/storage.tf new file mode 100644 index 0000000..f9ac9d9 --- /dev/null +++ b/distribution/terraform/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/distribution/terraform/templates/cloud-init.sh.tpl b/distribution/terraform/templates/cloud-init.sh.tpl new file mode 100644 index 0000000..90e8afd --- /dev/null +++ b/distribution/terraform/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/distribution/terraform/terraform.tfvars.example b/distribution/terraform/terraform.tfvars.example new file mode 100644 index 0000000..40f41e2 --- /dev/null +++ b/distribution/terraform/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-oke/variables.tf b/distribution/terraform/variables.tf similarity index 79% rename from deploy/terraform-oke/variables.tf rename to distribution/terraform/variables.tf index fcc6362..86d911c 100644 --- a/deploy/terraform-oke/variables.tf +++ b/distribution/terraform/variables.tf @@ -50,69 +50,51 @@ variable "public_subnet_cidr" { default = "10.0.1.0/24" } -variable "nodes_subnet_cidr" { - description = "CIDR block for the private subnet (OKE worker nodes)" +variable "private_subnet_cidr" { + description = "CIDR block for the private subnet (Compute)" type = string default = "10.0.2.0/24" } -variable "api_subnet_cidr" { - description = "CIDR block for the private subnet (OKE API endpoint)" +# ----------------------------------------------------------------------------- +# Compute Instance +# ----------------------------------------------------------------------------- +variable "instance_shape" { + description = "Compute instance shape" type = string - default = "10.0.3.0/24" + default = "VM.Standard.A1.Flex" } -# ----------------------------------------------------------------------------- -# 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" +variable "instance_ocpus" { + description = "Number of OCPUs for the flex instance" type = number default = 2 } -variable "node_memory_gb" { - description = "Memory in GB per worker node" +variable "instance_memory_gb" { + description = "Memory in GB for the flex instance" type = number default = 16 } -variable "node_count" { - description = "Number of worker nodes in the node pool" - type = number - default = 3 +variable "ssh_public_key" { + description = "SSH public key for instance access" + type = string } -variable "node_boot_volume_gb" { - description = "Boot volume size in GB per worker node" +# ----------------------------------------------------------------------------- +# Block Volume +# ----------------------------------------------------------------------------- +variable "block_volume_size_gb" { + description = "Size of the block volume in GB for /data" type = number default = 50 } -variable "ssh_public_key" { - description = "SSH public key for worker node access" - type = string +variable "block_volume_vpus_per_gb" { + description = "Volume performance units per GB (10=Balanced, 20=Higher, 30+=Ultra High)" + type = number + default = 20 } # ----------------------------------------------------------------------------- @@ -149,6 +131,17 @@ variable "ocir_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 @@ -170,12 +163,6 @@ variable "lb_max_bandwidth_mbps" { default = 100 } -variable "node_port" { - description = "NodePort for the Kubernetes frontend service (must match K8s Service spec)" - type = number - default = 30080 -} - # ----------------------------------------------------------------------------- # DNS (Optional) # ----------------------------------------------------------------------------- diff --git a/deploy/terraform-oke/waf.tf b/distribution/terraform/waf.tf similarity index 63% rename from deploy/terraform-oke/waf.tf rename to distribution/terraform/waf.tf index ec10121..c9d2d91 100644 --- a/deploy/terraform-oke/waf.tf +++ b/distribution/terraform/waf.tf @@ -1,82 +1,89 @@ # ----------------------------------------------------------------------------- -# OCI CIS Agent — OKE Deployment Stack -# WAF: Web Application Firewall Policy + Association +# WAF Policy # ----------------------------------------------------------------------------- - resource "oci_waf_web_app_firewall_policy" "main" { compartment_id = var.compartment_ocid - display_name = "cisagent-oke-waf-policy" + display_name = "cisagent-waf-policy" + freeform_tags = var.freeform_tags - # --- OWASP Protection Rules --- + # --- Actions --- actions { - name = "block" - type = "RETURN_HTTP_RESPONSE" - code = 403 - body { - type = "STATIC_TEXT" - text = "Blocked by WAF" - } + name = "checkAction" + type = "CHECK" } actions { - name = "allow" + name = "allowAction" type = "ALLOW" } - # --- Request Protection: XSS --- + 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 = "block" + action_name = "blockAction" 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 --- + # SQL Injection Protection rules { name = "sqli-protection" type = "PROTECTION" - action_name = "block" + action_name = "blockAction" is_body_inspection_enabled = true + protection_capabilities { + key = "942100" + version = 1 + } + protection_capabilities { key = "942110" version = 1 } - protection_capabilities { - key = "942120" - version = 1 - } - protection_capabilities { - key = "942130" - version = 1 - } } - # --- Request Protection: Path Traversal --- + # Path Traversal Protection rules { name = "path-traversal-protection" type = "PROTECTION" - action_name = "block" + action_name = "blockAction" is_body_inspection_enabled = true protection_capabilities { key = "930100" version = 1 } + protection_capabilities { key = "930110" version = 1 @@ -84,29 +91,28 @@ resource "oci_waf_web_app_firewall_policy" "main" { } } - # --- Rate Limiting: 300 requests/minute --- + # --- Rate Limiting --- request_rate_limiting { rules { - name = "rate-limit-300-per-minute" + name = "rate-limit-300-per-min" type = "RATE_LIMITING" - action_name = "block" + action_name = "blockAction" configurations { - period_in_seconds = 60 - requests_limit = 300 + period_in_seconds = 60 + requests_limit = 300 action_duration_in_seconds = 60 } } } - - freeform_tags = var.freeform_tags } -# --- Associate WAF with Load Balancer --- - +# ----------------------------------------------------------------------------- +# WAF Firewall (attached to Load Balancer) +# ----------------------------------------------------------------------------- resource "oci_waf_web_app_firewall" "main" { compartment_id = var.compartment_ocid - display_name = "cisagent-oke-waf" + 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