refactor: reorganize project structure — frontend/, infra/, clean paths
This commit is contained in:
6
infra/docker/Dockerfile.frontend
Normal file
6
infra/docker/Dockerfile.frontend
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM nginx:alpine
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY frontend/dist /usr/share/nginx/html/app
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
51
infra/docker/Dockerfile.single
Normal file
51
infra/docker/Dockerfile.single
Normal file
@@ -0,0 +1,51 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# ── System deps + Chromium + Terraform + nginx + supervisor ─────────────────
|
||||
ARG TERRAFORM_VERSION=1.14.7
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl gcc libffi-dev unzip \
|
||||
chromium fonts-liberation fonts-dejavu-core fontconfig \
|
||||
nginx supervisor && \
|
||||
ARCH=$(dpkg --print-architecture) && \
|
||||
curl -fsSL "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_${ARCH}.zip" -o /tmp/tf.zip && \
|
||||
unzip /tmp/tf.zip -d /usr/local/bin/ && rm /tmp/tf.zip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ── Python deps + OCI CLI ───────────────────────────────────────────────────
|
||||
COPY backend/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt && \
|
||||
pip install --no-cache-dir oci-cli
|
||||
|
||||
# ── Backend app ─────────────────────────────────────────────────────────────
|
||||
COPY backend/app.py .
|
||||
COPY backend/cis_reports.py .
|
||||
COPY backend/mcp_cis_server.py .
|
||||
COPY backend/gen_tf_reference.py .
|
||||
|
||||
# ── Frontend (pre-built React SPA) ─────────────────────────────────────────
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
COPY frontend/dist /usr/share/nginx/html/app
|
||||
|
||||
# ── Nginx config (proxy to localhost instead of backend hostname) ───────────
|
||||
RUN printf 'map $uri $empty {\n default "";\n}\n\nserver {\n listen 8080;\n server_name _;\n client_max_body_size 50M;\n absolute_redirect off;\n\n location / {\n root /usr/share/nginx/html/app;\n try_files $uri $uri/ /index.html;\n add_header Cache-Control "no-cache, no-store, must-revalidate" always;\n add_header X-Frame-Options "DENY" always;\n add_header X-Content-Type-Options "nosniff" always;\n add_header X-XSS-Protection "1; mode=block" always;\n add_header Referrer-Policy "strict-origin-when-cross-origin" always;\n add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;\n }\n\n location /api/ {\n proxy_pass http://127.0.0.1:8000/api/;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_read_timeout 900s;\n proxy_send_timeout 900s;\n proxy_connect_timeout 60s;\n add_header X-Frame-Options "SAMEORIGIN" always;\n add_header X-Content-Type-Options "nosniff" always;\n }\n}\n' > /etc/nginx/sites-available/default
|
||||
|
||||
# ── Supervisord config ──────────────────────────────────────────────────────
|
||||
RUN printf '[supervisord]\nnodaemon=true\nuser=root\nlogfile=/var/log/supervisor/supervisord.log\nlogfile_maxbytes=10MB\n\n[program:backend]\ncommand=runuser -u agent -- uvicorn app:app --host 127.0.0.1 --port 8000 --workers 8\ndirectory=/app\nautostart=true\nautorestart=true\nstdout_logfile=/dev/fd/1\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/fd/2\nstderr_logfile_maxbytes=0\n\n[program:nginx]\ncommand=nginx -g "daemon off;"\nautostart=true\nautorestart=true\nstdout_logfile=/dev/fd/1\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/fd/2\nstderr_logfile_maxbytes=0\n' > /etc/supervisor/conf.d/app.conf
|
||||
|
||||
# ── Non-root user + data dir ────────────────────────────────────────────────
|
||||
RUN groupadd -r agent && useradd -r -g agent -d /app -s /bin/bash agent && \
|
||||
mkdir -p /data /var/log/supervisor && chown -R agent:agent /app /data
|
||||
|
||||
# ── Generate Terraform resource reference ───────────────────────────────────
|
||||
RUN python gen_tf_reference.py || echo "WARN: tf reference generation skipped"
|
||||
|
||||
# ── Entrypoint ──────────────────────────────────────────────────────────────
|
||||
RUN printf '#!/bin/bash\nchown -R agent:agent /data 2>/dev/null || true\nexec supervisord -c /etc/supervisor/conf.d/app.conf\n' > /entrypoint.sh && \
|
||||
chmod +x /entrypoint.sh
|
||||
|
||||
VOLUME /data
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
131
infra/scripts/ocir-login.py
Executable file
131
infra/scripts/ocir-login.py
Executable file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OCIR Login Helper — Decrypts embedded credentials and performs docker login.
|
||||
|
||||
The auth token is encrypted with Fernet (AES-128-CBC + HMAC-SHA256).
|
||||
A passphrase is required to decrypt — distribute it out-of-band (Slack, email).
|
||||
|
||||
Usage:
|
||||
python ocir-login.py
|
||||
python ocir-login.py --passphrase <passphrase>
|
||||
python ocir-login.py --generate # Admin: encrypt a new token
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import getpass
|
||||
import hashlib
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# ── OCIR Configuration ─────────────────────────────────────────────────────
|
||||
OCIR_REGION = "us-ashburn-1"
|
||||
OCIR_ENDPOINT = f"{OCIR_REGION}.ocir.io"
|
||||
OCIR_NAMESPACE = "idi1o0a010nx"
|
||||
OCIR_USERNAME = f"{OCIR_NAMESPACE}/oracleidentitycloudservice/gustavo.nogueira@oracle.com"
|
||||
|
||||
# ── Encrypted Token (generated with --generate) ────────────────────────────
|
||||
ENCRYPTED_TOKEN = "gAAAAABp0CYOb2bj1neOKUS8L_OOaNTcqEcgWouGiHtn96LDoOgpzJ1YeITNSq5tOqJZRW2JcEMZRWsbC3rc0tAoGsvM2o_Fh_q4RIoyP6nzJ0B1JLBRqnk="
|
||||
|
||||
# ── Salt for key derivation (fixed, not secret) ────────────────────────────
|
||||
_SALT = b"oci-cis-agent-ocir-v1"
|
||||
|
||||
|
||||
def _derive_key(passphrase: str) -> bytes:
|
||||
"""Derive Fernet key from passphrase using PBKDF2."""
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=_SALT,
|
||||
iterations=600_000,
|
||||
)
|
||||
return base64.urlsafe_b64encode(kdf.derive(passphrase.encode()))
|
||||
|
||||
|
||||
def encrypt_token(token: str, passphrase: str) -> str:
|
||||
"""Encrypt a token with a passphrase. Returns base64 string."""
|
||||
from cryptography.fernet import Fernet
|
||||
key = _derive_key(passphrase)
|
||||
f = Fernet(key)
|
||||
return f.encrypt(token.encode()).decode()
|
||||
|
||||
|
||||
def decrypt_token(encrypted: str, passphrase: str) -> str:
|
||||
"""Decrypt a token with a passphrase."""
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
key = _derive_key(passphrase)
|
||||
f = Fernet(key)
|
||||
try:
|
||||
return f.decrypt(encrypted.encode()).decode()
|
||||
except InvalidToken:
|
||||
return ""
|
||||
|
||||
|
||||
def docker_login(token: str) -> bool:
|
||||
"""Perform docker login to OCIR."""
|
||||
result = subprocess.run(
|
||||
["docker", "login", OCIR_ENDPOINT, "-u", OCIR_USERNAME, "--password-stdin"],
|
||||
input=token,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print(f"Login succeeded: {OCIR_ENDPOINT}")
|
||||
return True
|
||||
else:
|
||||
print(f"Login failed: {result.stderr.strip()}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="OCIR Login Helper")
|
||||
parser.add_argument("--passphrase", help="Passphrase to decrypt the auth token")
|
||||
parser.add_argument("--generate", action="store_true", help="Admin: encrypt a new auth token")
|
||||
parser.add_argument("--pull", action="store_true", help="Also pull the image after login")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.generate:
|
||||
print("=== Generate Encrypted Token ===")
|
||||
token = getpass.getpass("Auth Token (from OCI Console): ")
|
||||
passphrase = getpass.getpass("Passphrase (share with team): ")
|
||||
passphrase2 = getpass.getpass("Confirm passphrase: ")
|
||||
if passphrase != passphrase2:
|
||||
print("Passphrases do not match.")
|
||||
sys.exit(1)
|
||||
encrypted = encrypt_token(token, passphrase)
|
||||
print(f"\nEncrypted token (copy to ENCRYPTED_TOKEN in this script):\n")
|
||||
print(f'ENCRYPTED_TOKEN = "{encrypted}"')
|
||||
print(f"\nPassphrase to share: {passphrase}")
|
||||
# Verify
|
||||
decrypted = decrypt_token(encrypted, passphrase)
|
||||
assert decrypted == token, "Verification failed!"
|
||||
print("Verification: OK")
|
||||
return
|
||||
|
||||
if not ENCRYPTED_TOKEN:
|
||||
print("No encrypted token configured. Run with --generate first.")
|
||||
sys.exit(1)
|
||||
|
||||
passphrase = args.passphrase or getpass.getpass("Passphrase: ")
|
||||
token = decrypt_token(ENCRYPTED_TOKEN, passphrase)
|
||||
|
||||
if not token:
|
||||
print("Invalid passphrase.")
|
||||
sys.exit(1)
|
||||
|
||||
success = docker_login(token)
|
||||
|
||||
if success and args.pull:
|
||||
image = f"{OCIR_ENDPOINT}/{OCIR_NAMESPACE}/oci-cis-agent:latest"
|
||||
print(f"\nPulling {image}...")
|
||||
subprocess.run(["docker", "pull", image])
|
||||
|
||||
# Clear sensitive data
|
||||
token = None
|
||||
passphrase = None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
49
infra/scripts/push-images.sh
Executable file
49
infra/scripts/push-images.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
OCIR_REGION="${OCIR_REGION:?Set OCIR_REGION (e.g., us-ashburn-1)}"
|
||||
OCIR_NAMESPACE="${OCIR_NAMESPACE:?Set OCIR_NAMESPACE}"
|
||||
OCIR_REPO_PREFIX="${OCIR_REPO_PREFIX:-oci-cis-agent}"
|
||||
IMAGE_TAG="${IMAGE_TAG:-latest}"
|
||||
|
||||
UNIFIED="${OCIR_REGION}.ocir.io/${OCIR_NAMESPACE}/${OCIR_REPO_PREFIX}:${IMAGE_TAG}"
|
||||
|
||||
echo "========================================"
|
||||
echo "OCI CIS Agent — Build & Push to OCIR"
|
||||
echo "========================================"
|
||||
echo "Image: $UNIFIED"
|
||||
echo ""
|
||||
|
||||
# 1. Build frontend React SPA
|
||||
echo ">>> Building frontend..."
|
||||
cd "$PROJECT_ROOT/frontend"
|
||||
npm ci --silent
|
||||
npm run build
|
||||
echo "Frontend build OK."
|
||||
|
||||
# 2. Ensure buildx builder exists
|
||||
docker buildx inspect multiarch >/dev/null 2>&1 || \
|
||||
docker buildx create --name multiarch --use
|
||||
docker buildx use multiarch
|
||||
|
||||
# 3. Login to OCIR
|
||||
echo ">>> Logging in to OCIR..."
|
||||
docker login "${OCIR_REGION}.ocir.io"
|
||||
|
||||
# 4. Build + push single container image (multi-arch)
|
||||
echo ">>> Building & pushing image (amd64 + arm64)..."
|
||||
cd "$PROJECT_ROOT"
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
-t "$UNIFIED" \
|
||||
--push \
|
||||
-f infra/docker/Dockerfile.single .
|
||||
echo "Image pushed."
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Done! $UNIFIED"
|
||||
echo "========================================"
|
||||
78
infra/terraform-vm/compute.tf
Normal file
78
infra/terraform-vm/compute.tf
Normal 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
infra/terraform-vm/dns.tf
Normal file
36
infra/terraform-vm/dns.tf
Normal 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
infra/terraform-vm/lb.tf
Normal file
143
infra/terraform-vm/lb.tf
Normal 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
infra/terraform-vm/network.tf
Normal file
189
infra/terraform-vm/network.tf
Normal 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
infra/terraform-vm/outputs.tf
Normal file
56
infra/terraform-vm/outputs.tf
Normal 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
infra/terraform-vm/provider.tf
Normal file
22
infra/terraform-vm/provider.tf
Normal 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
|
||||
}
|
||||
22
infra/terraform-vm/storage.tf
Normal file
22
infra/terraform-vm/storage.tf
Normal 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]
|
||||
}
|
||||
141
infra/terraform-vm/templates/cloud-init.sh.tpl
Normal file
141
infra/terraform-vm/templates/cloud-init.sh.tpl
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/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
|
||||
|
||||
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
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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 image
|
||||
# -----------------------------------------------------------------------------
|
||||
IMAGE="$OCIR_ENDPOINT/${ocir_namespace}/${ocir_repo_prefix}:latest"
|
||||
|
||||
echo ">>> Pulling image: $IMAGE"
|
||||
docker pull "$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:
|
||||
agent:
|
||||
image: ${ocir_region}.ocir.io/${ocir_namespace}/${ocir_repo_prefix}:latest
|
||||
container_name: oci-cis-agent
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- APP_SECRET=${app_secret}
|
||||
- JWT_EXPIRY_HOURS=${jwt_expiry_hours}
|
||||
- DATA_DIR=/data
|
||||
- CORS_ORIGINS=${cors_origins}
|
||||
- TZ=${app_timezone}
|
||||
- OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=True
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /data:/data
|
||||
mem_limit: 4g
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
COMPOSE_EOF
|
||||
|
||||
echo ">>> docker-compose.yml written to $APP_DIR"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 7. Start application
|
||||
# -----------------------------------------------------------------------------
|
||||
echo ">>> Starting application..."
|
||||
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 ">>> Container status:"
|
||||
docker compose ps
|
||||
echo ">>> Logs:"
|
||||
docker compose logs --tail 50
|
||||
fi
|
||||
|
||||
echo "=== OCI CIS Agent cloud-init finished at $(date) ==="
|
||||
55
infra/terraform-vm/terraform.tfvars.example
Normal file
55
infra/terraform-vm/terraform.tfvars.example
Normal 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
infra/terraform-vm/variables.tf
Normal file
185
infra/terraform-vm/variables.tf
Normal 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
infra/terraform-vm/waf.tf
Normal file
120
infra/terraform-vm/waf.tf
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user