v1.0 — AI Agent Distribution (Docker Compose + Terraform OCI)

This commit is contained in:
nogueiraguh
2026-04-02 14:26:33 -03:00
commit 5699c9457f
15 changed files with 1399 additions and 0 deletions

29
.env.example Normal file
View File

@@ -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

233
README.md Normal file
View File

@@ -0,0 +1,233 @@
<p align="center">
<img src="logo.svg" alt="AI Agent" width="96" height="96">
</p>
<h1 align="center">AI Agent — Infrastructure & Security Engineer</h1>
<p align="center">
<strong>Oracle Cloud Infrastructure — CIS Foundations Benchmark 3.0 — AI-Powered Compliance Platform</strong>
</p>
<p align="center">
<img src="https://img.shields.io/badge/version-1.0-C74634?style=flat-square" alt="Version">
<img src="https://img.shields.io/badge/OCI-GenAI-C74634?style=flat-square" alt="OCI">
<img src="https://img.shields.io/badge/docker-compose-2496ED?style=flat-square" alt="Docker">
<img src="https://img.shields.io/badge/terraform-7B42BC?style=flat-square" alt="Terraform">
</p>
---
## 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=<generate with: openssl rand -hex 64>
```
### 2. Login to OCIR
```bash
docker login ${OCIR_REGION}.ocir.io
```
Username: `<namespace>/<username>` (or `<namespace>/oracleidentitycloudservice/<email>`)
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://<IP>
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 <group-name> to use generative-ai-family in compartment <compartment-name>
Allow group <group-name> to read all-resources in tenancy
Allow group <group-name> to inspect compartments in tenancy
Allow group <group-name> to inspect autonomous-databases in compartment <compartment-name>
Allow group <group-name> to read virtual-network-family in compartment <compartment-name>
Allow group <group-name> to read instance-family in compartment <compartment-name>
Allow group <group-name> to read objectstorage-namespaces in tenancy
Allow group <group-name> to read buckets in compartment <compartment-name>
```
---
## 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
---
<p align="center">
<sub>Built for Oracle Cloud Infrastructure security compliance</sub>
</p>

54
docker-compose.yml Normal file
View File

@@ -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

78
terraform/compute.tf Normal file
View File

@@ -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"],
]
}
}

36
terraform/dns.tf Normal file
View File

@@ -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.

143
terraform/lb.tf Normal file
View File

@@ -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]
}

189
terraform/network.tf Normal file
View File

@@ -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
}

56
terraform/outputs.tf Normal file
View File

@@ -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"
}

22
terraform/provider.tf Normal file
View File

@@ -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
}

14
terraform/registry.tf Normal file
View File

@@ -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
}

22
terraform/storage.tf Normal file
View File

@@ -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]
}

View File

@@ -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) ==="

View File

@@ -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 <REQUIRED>.
# =============================================================================
# --- OCI Authentication ---
tenancy_ocid = "<REQUIRED>"
user_ocid = "<REQUIRED>"
fingerprint = "<REQUIRED>"
private_key_path = "<REQUIRED>" # e.g. "~/.oci/oci_api_key.pem"
region = "us-ashburn-1"
# --- Compartment ---
compartment_ocid = "<REQUIRED>"
# --- 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 = "<REQUIRED>" # contents of ~/.ssh/id_rsa.pub
# --- Block Volume ---
# block_volume_size_gb = 50
# block_volume_vpus_per_gb = 20
# --- Application ---
app_secret = "<REQUIRED>" # generate with: python3 -c "import secrets; print(secrets.token_hex(64))"
# jwt_expiry_hours = 12
# app_timezone = "UTC"
# --- OCIR ---
ocir_namespace = "<REQUIRED>" # tenancy Object Storage namespace
ocir_username = "<REQUIRED>" # e.g. "user@email.com" (without namespace prefix)
ocir_auth_token = "<REQUIRED>" # 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"
# }

185
terraform/variables.tf Normal file
View File

@@ -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"
}
}

120
terraform/waf.tf Normal file
View File

@@ -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
}