From 5699c9457fcecc49c5a8e086d7cf3ac5748f988b Mon Sep 17 00:00:00 2001 From: nogueiraguh Date: Thu, 2 Apr 2026 14:26:33 -0300 Subject: [PATCH] =?UTF-8?q?v1.0=20=E2=80=94=20AI=20Agent=20Distribution=20?= =?UTF-8?q?(Docker=20Compose=20+=20Terraform=20OCI)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 29 ++++ README.md | 233 ++++++++++++++++++++++++++ docker-compose.yml | 54 ++++++ terraform/compute.tf | 78 +++++++++ terraform/dns.tf | 36 ++++ terraform/lb.tf | 143 ++++++++++++++++ terraform/network.tf | 189 +++++++++++++++++++++ terraform/outputs.tf | 56 +++++++ terraform/provider.tf | 22 +++ terraform/registry.tf | 14 ++ terraform/storage.tf | 22 +++ terraform/templates/cloud-init.sh.tpl | 163 ++++++++++++++++++ terraform/terraform.tfvars.example | 55 ++++++ terraform/variables.tf | 185 ++++++++++++++++++++ terraform/waf.tf | 120 +++++++++++++ 15 files changed, 1399 insertions(+) create mode 100644 .env.example create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 terraform/compute.tf create mode 100644 terraform/dns.tf create mode 100644 terraform/lb.tf create mode 100644 terraform/network.tf create mode 100644 terraform/outputs.tf create mode 100644 terraform/provider.tf create mode 100644 terraform/registry.tf create mode 100644 terraform/storage.tf create mode 100644 terraform/templates/cloud-init.sh.tpl create mode 100644 terraform/terraform.tfvars.example create mode 100644 terraform/variables.tf create mode 100644 terraform/waf.tf diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..aa8cc1e --- /dev/null +++ b/.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/README.md b/README.md new file mode 100644 index 0000000..1289932 --- /dev/null +++ b/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/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..346e598 --- /dev/null +++ b/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/terraform/compute.tf b/terraform/compute.tf new file mode 100644 index 0000000..fb12833 --- /dev/null +++ b/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/terraform/dns.tf b/terraform/dns.tf new file mode 100644 index 0000000..cef7882 --- /dev/null +++ b/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/terraform/lb.tf b/terraform/lb.tf new file mode 100644 index 0000000..8202b78 --- /dev/null +++ b/terraform/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/terraform/network.tf b/terraform/network.tf new file mode 100644 index 0000000..5ceb520 --- /dev/null +++ b/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/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..70d7aa3 --- /dev/null +++ b/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/terraform/provider.tf b/terraform/provider.tf new file mode 100644 index 0000000..0b8d9ba --- /dev/null +++ b/terraform/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/terraform/registry.tf b/terraform/registry.tf new file mode 100644 index 0000000..b9bf1ad --- /dev/null +++ b/terraform/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/terraform/storage.tf b/terraform/storage.tf new file mode 100644 index 0000000..f9ac9d9 --- /dev/null +++ b/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/terraform/templates/cloud-init.sh.tpl b/terraform/templates/cloud-init.sh.tpl new file mode 100644 index 0000000..90e8afd --- /dev/null +++ b/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/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example new file mode 100644 index 0000000..40f41e2 --- /dev/null +++ b/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/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..86d911c --- /dev/null +++ b/terraform/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/terraform/waf.tf b/terraform/waf.tf new file mode 100644 index 0000000..c9d2d91 --- /dev/null +++ b/terraform/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 +}