OAK, MAPLE & CEDAR — GCP Cloud Server Deployment Guide¶
cloud.us | GCP us-central1 | Rocky Linux 8.10 | CIS Controls v8¶
Servers: oak.cloud.us — 172.16.0.10 (Security Scanner) · maple.cloud.us — 172.16.0.12 (Cloud Monitoring) · cedar.cloud.us — 172.16.0.13 (Cloud Logging)
VPC: gpus-vpc — 172.16.0.0/24 · GCP project gpus-infra · Region us-central1
DNS: cloud.us authoritative zone on SKY (192.168.120.1) · replicated to RAIN (192.168.120.2)
VPN: All cloud VMs reachable from WDC over IPSec tunnel (130.211.194.72 ↔ 38.140.146.68)
Platform: Rocky Linux 8.10 · GCP Compute Engine · OAK: n2-standard-2 · MAPLE/CEDAR: e2-standard
Framework: CIS Controls v8 · CIS Rocky Linux 8 Benchmark
Stack (OAK): Docker CE 26.1.3 · Greenbone Community Container (OpenVAS) · node_exporter · Wazuh Agent · AIDE · Fail2ban · auditd · Lynis · SELinux
Stack (MAPLE): Prometheus v2.51.0 · Grafana · Wazuh Manager · Filebeat · node_exporter · AIDE · Fail2ban · auditd · Lynis · SELinux
Stack (CEDAR): Elasticsearch 8.19.13 · Logstash 8.19.13 · Kibana 8.19.13 · Wazuh Agent · node_exporter · AIDE · Fail2ban · auditd · Lynis · SELinux
Scan Targets (OAK): All WDC + GCP VMs · Weekly Sunday 01:00
Monitoring Targets (MAPLE): OAK · MAPLE (self) · CEDAR · + WDC servers over VPN
Log Sources (CEDAR): OAK · MAPLE · CEDAR + WDC servers (rsyslog → CEDAR:5140)
Admin: rajesh.chhetry@greenpeace.us
Document Version: v2.0 — 2026-03-24
Build Status: ✅ OAK COMPLETE · ✅ MAPLE COMPLETE · ✅ CEDAR COMPLETE
Classification: CONFIDENTIAL — Internal Use Only
Architecture Overview¶
GCP VPC — 172.16.0.0/24 (us-central1)
│
┌──────────────┼──────────────┐
│ │ │
172.16.0.10 172.16.0.12 172.16.0.13
OAK MAPLE CEDAR
OpenVAS Scanner Prometheus ELK Stack
Wazuh Agent Grafana Wazuh Indexer
Wazuh Manager
│ │ │
└──────────────┼──────────────┘
│ VPN Tunnel (IKEv2 / AES-256)
│
┌──────────────┴──────────────┐
│ │
192.168.120.1 192.168.120.3
SKY (DNS) SUN (WDC Mon)
192.168.120.2 192.168.120.4
RAIN (DNS) WIND (WDC Log)
OAK scan flow (Sun 01:00):
OAK → OpenVAS → scans all WDC + GCP IPs over VPN
MAPLE monitoring flow:
MAPLE:9090 ← node_exporter scrape ← OAK/MAPLE/CEDAR (cloud VMs)
MAPLE:9090 ← node_exporter scrape ← SKY/RAIN/SUN/WIND (over VPN)
MAPLE:3000 (Grafana) — unified dashboards
CEDAR logging flow:
All VMs rsyslog → CEDAR:5140 (Logstash) → CEDAR:9200 (ES) → CEDAR:5601 (Kibana)
Wazuh flow:
Wazuh agents (all 7 VMs) → MAPLE:1514 (Wazuh Manager) → alerts
MAPLE Wazuh Manager → Filebeat (MAPLE) → CEDAR:9200 (wazuh-alerts-* index) → Kibana SIEM
CIS Controls Quick Reference¶
| CIS Control | Title | Implementation in This Guide |
|---|---|---|
| CIS 1 | Inventory & Control of Enterprise Assets | cloud.us DNS records; host registry entries for OAK/MAPLE/CEDAR |
| CIS 2 | Inventory & Control of Software Assets | Minimal package install, dnf-automatic security updates |
| CIS 3 | Data Protection | TLS on Webmin, encrypted management, GCS backup via instance SA |
| CIS 4 | Secure Configuration | CIS Rocky Linux 8 Benchmark; service hardening |
| CIS 5 | Account Management | cloudadmin account, sudo logging, SSH key-only auth |
| CIS 6 | Access Control Management | SELinux enforcing, firewalld default-drop, VPN-only access |
| CIS 7 | Continuous Vulnerability Management | OpenVAS weekly scans on OAK; dnf-automatic on all three |
| CIS 8 | Audit Log Management | auditd immutable (-e 2); centralized logging to CEDAR |
| CIS 10 | Malware Defenses | SELinux enforcing; AIDE daily integrity monitoring |
| CIS 11 | Data Recovery | gpus-backup.sh daily → GCS via instance service account |
| CIS 12 | Network Infrastructure Management | VPC firewall default-deny; VPN-only from WDC |
| CIS 13 | Network Monitoring & Defense | Prometheus + Grafana (MAPLE); Kibana dashboards (CEDAR) |
| CIS 17 | Incident Response | Wazuh alerts; AIDE alerts; full auditd trail |
Table of Contents¶
- Network & IP Allocation Summary
- GCP VM Provisioning — Terraform
- Rocky Linux 8.10 — OS Bootstrap
- CIS Baseline Hardening
- Account & Access Management
- Firewall Configuration
- Disk Layout — Second Persistent Disk (LVM)
- Webmin GUI Management
- OAK — Docker CE + Greenbone/OpenVAS
- MAPLE — Prometheus + Grafana + Wazuh Manager
- CEDAR — ELK Stack + Wazuh Indexer
- Wazuh Agents — All 7 Servers
- Prometheus Exporters — All Cloud VMs
- Log Forwarding — All VMs to CEDAR
- Backup Configuration — GCS via Instance SA
- File Integrity Monitoring — AIDE
- DNS Registration — cloud.us zone
- Status Backend Integration
- Verification & Testing
- Post-Change Checklist
1. Network & IP Allocation Summary¶
CIS Controls: CIS 1.1 (Asset Inventory) · CIS 4.1 (Secure Configuration)
1.1 Server Identity¶
| Parameter | OAK | MAPLE | CEDAR |
|---|---|---|---|
| Hostname | oak.cloud.us | maple.cloud.us | cedar.cloud.us |
| IP Address | 172.16.0.10 | 172.16.0.12 | 172.16.0.13 |
| Gateway | 172.16.0.1 (GCP) | 172.16.0.1 (GCP) | 172.16.0.1 (GCP) |
| DNS | 192.168.120.1 (SKY over VPN) | 192.168.120.1 (SKY over VPN) | 192.168.120.1 (SKY over VPN) |
| Role | OpenVAS/Greenbone Scanner | Prometheus + Grafana + Wazuh Mgr | ELK Stack + Wazuh Indexer |
| OS | Rocky Linux 8.10 | Rocky Linux 8.10 | Rocky Linux 8.10 |
| Machine Type | n2-standard-2 | e2-standard-2 | e2-standard-4 |
| vCPUs | 2 | 2 | 4 |
| RAM | 8 GB | 8 GB | 16 GB |
| Boot Disk (sda) | 50 GB pd-ssd | 50 GB pd-ssd | 50 GB pd-ssd |
| Data Disk (sdb) | 50 GB pd-ssd | 50 GB pd-ssd | 50 GB pd-ssd |
| Admin User | cloudadmin | cloudadmin | cloudadmin |
| Service Account | oak-agent@gpus-infra | maple-agent@gpus-infra | cedar-agent@gpus-infra |
1.2 Service Ports¶
| Server | Port | Service | Access |
|---|---|---|---|
| OAK | 9392 | OpenVAS nginx (HTTPS, localhost:443 backend) | Localhost only (SSH tunnel) |
| OAK | 9100 | node_exporter | MAPLE only (172.16.0.12) |
| OAK | 1514 | Wazuh agent | MAPLE only (172.16.0.12) |
| OAK | 22 | SSH | 10.8.0.0/28 (VPN tunnel) |
| MAPLE | 9090 | Prometheus UI | 10.8.0.0/28 + VPC |
| MAPLE | 3000 | Grafana | 10.8.0.0/28 + VPC |
| MAPLE | 1514 | Wazuh Manager (agent) | All VMs in VPC + WDC via VPN |
| MAPLE | 1515 | Wazuh Manager (enroll) | All VMs in VPC + WDC via VPN |
| MAPLE | 9100 | node_exporter | Localhost + MAPLE |
| MAPLE | 10000 | Webmin | 10.8.0.0/28 |
| MAPLE | 22 | SSH | 10.8.0.0/28 |
| CEDAR | 5140 | Logstash syslog input | All VMs in VPC + WDC via VPN |
| CEDAR | 9200 | Elasticsearch | Localhost + MAPLE (Wazuh) |
| CEDAR | 5601 | Kibana | 10.8.0.0/28 + VPC |
| CEDAR | 9100 | node_exporter | MAPLE only |
| CEDAR | 10000 | Webmin | 10.8.0.0/28 |
| CEDAR | 22 | SSH | 10.8.0.0/28 |
1.3 Disk Layout — sdb (Data Disk, 50 GB pd-ssd)¶
╔══════════════════════════╦══════════════════════════════════════════╗
║ OAK (Scanner) ║ MAPLE (Monitoring) ║
╠══════════════════════════╬══════════════════════════════════════════╣
║ /var/log 15 GB ║ /var/log 10 GB ║
║ /var/log/audit 10 GB ║ /var/log/audit 8 GB ║
║ /var/lib/openvas ║ /var/lib/prometheus 25 GB ║
║ 15 GB ║ /backup 5 GB ║
║ /backup 8 GB ║ FREE ~2 GB ║
║ FREE ~2 GB ║ ║
╠══════════════════════════╬══════════════════════════════════════════╣
║ CEDAR (Logging/SIEM) ║ All sdb volumes in VG: data ║
╠══════════════════════════╣ sdb device ONLY — sda excluded ║
║ /var/log 10 GB ║ ║
║ /var/log/audit 8 GB ║ CIS 8.3: Dedicated log partitions ║
║ /var/lib/elasticsearch ║ ║
║ 20 GB ║ CIS 3.11: Sensitive data protected ║
║ /var/lib/logstash ║ by partition isolation ║
║ 8 GB ║ ║
║ /backup 2 GB ║ SSD quota: 500GB total us-central1 ║
║ FREE ~2 GB ║ 300GB used (3×100GB). Max 5 VMs. ║
╚══════════════════════════╩══════════════════════════════════════════╝
2. GCP VM Provisioning — Terraform¶
2.1 oak.tf — OAK Security Scanner¶
# ═══════════════════════════════════════════════════════════
# oak.tf — OAK GCP VM — OpenVAS/Greenbone Security Scanner
# IP: 172.16.0.10 | Role: Vulnerability scanning
# ═══════════════════════════════════════════════════════════
resource "google_service_account" "oak_sa" {
account_id = "oak-sa"
display_name = "OAK VM Service Account"
description = "Instance SA for OAK — GCS backup access"
}
resource "google_storage_bucket_iam_member" "oak_backup" {
bucket = "gpus-infra-backups-wdc"
role = "roles/storage.objectAdmin"
member = "serviceAccount:${google_service_account.oak_sa.email}"
}
resource "google_compute_disk" "oak_data" {
name = "oak-data"
type = "pd-ssd"
zone = var.zone
size = 300
}
resource "google_compute_instance" "oak" {
name = "oak"
machine_type = "e2-standard-2"
zone = var.zone
boot_disk {
initialize_params {
image = "rocky-linux-cloud/rocky-linux-8-optimized-gcp"
size = 200
type = "pd-ssd"
}
}
attached_disk {
source = google_compute_disk.oak_data.id
mode = "READ_WRITE"
}
network_interface {
subnetwork = google_compute_subnetwork.gpus_subnet.id
network_ip = "172.16.0.10"
}
service_account {
email = google_service_account.oak_sa.email
scopes = ["cloud-platform"]
}
metadata = {
enable-oslogin = "false"
block-project-ssh-keys = "true"
}
tags = ["gpus-vm", "openvas-scanner"]
labels = {
role = "security-scanner"
environment = "production"
managed-by = "terraform"
}
}
2.2 maple.tf — MAPLE Cloud Monitoring¶
# ═══════════════════════════════════════════════════════════
# maple.tf — MAPLE GCP VM — Prometheus + Grafana + Wazuh Manager
# IP: 172.16.0.12 | Role: Cloud monitoring stack
# ═══════════════════════════════════════════════════════════
resource "google_service_account" "maple_sa" {
account_id = "maple-sa"
display_name = "MAPLE VM Service Account"
description = "Instance SA for MAPLE — GCS backup access"
}
resource "google_storage_bucket_iam_member" "maple_backup" {
bucket = "gpus-infra-backups-wdc"
role = "roles/storage.objectAdmin"
member = "serviceAccount:${google_service_account.maple_sa.email}"
}
resource "google_compute_disk" "maple_data" {
name = "maple-data"
type = "pd-ssd"
zone = var.zone
size = 300
}
resource "google_compute_instance" "maple" {
name = "maple"
machine_type = "e2-standard-2"
zone = var.zone
boot_disk {
initialize_params {
image = "rocky-linux-cloud/rocky-linux-8-optimized-gcp"
size = 200
type = "pd-ssd"
}
}
attached_disk {
source = google_compute_disk.maple_data.id
mode = "READ_WRITE"
}
network_interface {
subnetwork = google_compute_subnetwork.gpus_subnet.id
network_ip = "172.16.0.12"
}
service_account {
email = google_service_account.maple_sa.email
scopes = ["cloud-platform"]
}
metadata = {
enable-oslogin = "false"
block-project-ssh-keys = "true"
}
tags = ["gpus-vm", "monitoring"]
labels = {
role = "monitoring"
environment = "production"
managed-by = "terraform"
}
}
2.3 cedar.tf — CEDAR Cloud Logging¶
# ═══════════════════════════════════════════════════════════
# cedar.tf — CEDAR GCP VM — ELK Stack + Wazuh Indexer
# IP: 172.16.0.13 | Role: Cloud logging + SIEM
# ═══════════════════════════════════════════════════════════
resource "google_service_account" "cedar_sa" {
account_id = "cedar-sa"
display_name = "CEDAR VM Service Account"
description = "Instance SA for CEDAR — GCS backup access"
}
resource "google_storage_bucket_iam_member" "cedar_backup" {
bucket = "gpus-infra-backups-wdc"
role = "roles/storage.objectAdmin"
member = "serviceAccount:${google_service_account.cedar_sa.email}"
}
resource "google_compute_disk" "cedar_data" {
name = "cedar-data"
type = "pd-ssd"
zone = var.zone
size = 300
}
resource "google_compute_instance" "cedar" {
name = "cedar"
machine_type = "e2-standard-4"
zone = var.zone
boot_disk {
initialize_params {
image = "rocky-linux-cloud/rocky-linux-8-optimized-gcp"
size = 200
type = "pd-ssd"
}
}
attached_disk {
source = google_compute_disk.cedar_data.id
mode = "READ_WRITE"
}
network_interface {
subnetwork = google_compute_subnetwork.gpus_subnet.id
network_ip = "172.16.0.13"
}
service_account {
email = google_service_account.cedar_sa.email
scopes = ["cloud-platform"]
}
metadata = {
enable-oslogin = "false"
block-project-ssh-keys = "true"
}
tags = ["gpus-vm", "logging"]
labels = {
role = "logging"
environment = "production"
managed-by = "terraform"
}
}
2.4 vpc.tf firewall additions¶
Add these rules to vpc.tf to allow monitoring and logging traffic within the VPC:
# Allow Prometheus scrape from MAPLE to all VMs
resource "google_compute_firewall" "allow_prometheus" {
name = "allow-prometheus-scrape"
network = google_compute_network.gpus_vpc.id
allow {
protocol = "tcp"
ports = ["9100", "9119"]
}
source_tags = ["monitoring"]
target_tags = ["gpus-vm"]
description = "Allow MAPLE Prometheus to scrape node_exporter on all VMs"
}
# Allow Logstash syslog from all VMs to CEDAR
resource "google_compute_firewall" "allow_logstash" {
name = "allow-logstash-syslog"
network = google_compute_network.gpus_vpc.id
allow {
protocol = "tcp"
ports = ["5140"]
}
source_tags = ["gpus-vm"]
target_tags = ["logging"]
description = "Allow rsyslog forwarding to CEDAR Logstash"
}
# Allow Wazuh agent connections to MAPLE
resource "google_compute_firewall" "allow_wazuh" {
name = "allow-wazuh-agents"
network = google_compute_network.gpus_vpc.id
allow {
protocol = "tcp"
ports = ["1514", "1515"]
}
allow {
protocol = "udp"
ports = ["1514"]
}
source_tags = ["gpus-vm"]
target_tags = ["monitoring"]
description = "Allow Wazuh agents to connect to MAPLE Wazuh Manager"
}
2.5 Apply¶
cd ~/terraform/gpus-infra/terraform
terraform plan -out=tfplan
terraform apply tfplan
# Verify instances
gcloud compute instances list --filter="name:(oak OR maple OR cedar)"
3. Rocky Linux 8.10 — OS Bootstrap¶
Run on each new VM after first SSH login. The GCP image boots directly to Rocky Linux 8.
3.1 Initial SSH Access¶
# SSH from WDC or your Mac (over VPN)
ssh -i ~/.ssh/gpus-cloudadmin cloudadmin@172.16.0.10 # OAK
ssh -i ~/.ssh/gpus-cloudadmin cloudadmin@172.16.0.12 # MAPLE
ssh -i ~/.ssh/gpus-cloudadmin cloudadmin@172.16.0.13 # CEDAR
3.2 Set Hostname¶
# Run on each — substitute correct hostname
sudo hostnamectl set-hostname oak.cloud.us # or maple / cedar
sudo sed -i 's/^HOSTNAME=.*/HOSTNAME=oak.cloud.us/' /etc/sysconfig/network
3.3 Configure DNS (SKY over VPN)¶
sudo nmcli connection modify "System eth0" ipv4.dns "192.168.120.1 192.168.120.2"
sudo nmcli connection modify "System eth0" ipv4.ignore-auto-dns yes
sudo nmcli connection up "System eth0"
# Verify
dig oak.cloud.us @192.168.120.1
3.4 System Update¶
sudo dnf update -y
sudo dnf install -y epel-release
sudo dnf install -y vim wget curl git tar unzip net-tools bind-utils \
htop lsof tcpdump rsync chrony firewalld
3.5 Time Synchronization¶
4. CIS Baseline Hardening¶
The same CIS hardening baseline applied to SKY/RAIN/SUN/WIND applies here. Key items:
4.1 Kernel Hardening (sysctl)¶
cat > /etc/sysctl.d/99-gpus-hardening.conf << 'EOF'
# CIS 3.1.1 — IP Forwarding
net.ipv4.ip_forward = 0
# CIS 3.2 — Network parameters
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# CIS 3.3 — IPv6 disabled
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
# CIS 1.5 — Core dump restriction
fs.suid_dumpable = 0
kernel.core_uses_pid = 1
# CIS 3.4 — Address space randomization
kernel.randomize_va_space = 2
EOF
sudo sysctl -p /etc/sysctl.d/99-gpus-hardening.conf
4.2 SSH Hardening (CIS 5.2)¶
cat > /etc/ssh/sshd_config.d/99-gpus-hardening.conf << 'EOF'
Protocol 2
PermitRootLogin no
MaxAuthTries 3
MaxSessions 3
PasswordAuthentication no
PermitEmptyPasswords no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
X11Forwarding no
AllowTcpForwarding no
ClientAliveInterval 300
ClientAliveCountMax 1
LoginGraceTime 60
AllowUsers cloudadmin
Banner /etc/issue.net
EOF
sudo systemctl restart sshd
4.3 SELinux¶
sudo sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
sudo setenforce 1
sudo getenforce # should return: Enforcing
4.4 Login Banner (CIS 1.7)¶
echo "AUTHORIZED ACCESS ONLY — Greenpeace USA IT Infrastructure — Unauthorized access is prohibited" | sudo tee /etc/issue.net
4.5 Password Policy (CIS 5.4)¶
sudo sed -i 's/^PASS_MAX_DAYS.*/PASS_MAX_DAYS 90/' /etc/login.defs
sudo sed -i 's/^PASS_MIN_DAYS.*/PASS_MIN_DAYS 1/' /etc/login.defs
sudo sed -i 's/^PASS_MIN_LEN.*/PASS_MIN_LEN 14/' /etc/login.defs
sudo sed -i 's/^PASS_WARN_AGE.*/PASS_WARN_AGE 14/' /etc/login.defs
4.6 auditd — Immutable Mode (CIS 8.2)¶
sudo dnf install -y audit audit-libs
cat >> /etc/audit/rules.d/gpus.rules << 'EOF'
# CIS 4.1.4 — Time change events
-a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time-change
-a always,exit -F arch=b32 -S adjtimex -S settimeofday -S stime -k time-change
-a always,exit -F arch=b64 -S clock_settime -k time-change
-w /etc/localtime -p wa -k time-change
# CIS 4.1.6 — User/group modification
-w /etc/group -p wa -k identity
-w /etc/passwd -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/security/opasswd -p wa -k identity
# CIS 4.1.7 — Network environment changes
-a always,exit -F arch=b64 -S sethostname -S setdomainname -k system-locale
-w /etc/issue -p wa -k system-locale
-w /etc/issue.net -p wa -k system-locale
-w /etc/hosts -p wa -k system-locale
# CIS 4.1.9 — DAC permission modification
-a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -F auid>=1000 -F auid!=4294967295 -k perm_mod
-a always,exit -F arch=b64 -S chown -S fchown -S fchownat -S lchown -F auid>=1000 -F auid!=4294967295 -k perm_mod
# CIS 4.1.10 — Unauthorized access attempts
-a always,exit -F arch=b64 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=4294967295 -k access
-a always,exit -F arch=b64 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=4294967295 -k access
# CIS 4.1.12 — Successful filesystem mounts
-a always,exit -F arch=b64 -S mount -F auid>=1000 -F auid!=4294967295 -k mounts
# CIS 4.1.14 — File deletion
-a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat -F auid>=1000 -F auid!=4294967295 -k delete
# CIS 4.1.15 — Sudo usage
-w /etc/sudoers -p wa -k scope
-w /etc/sudoers.d/ -p wa -k scope
-a always,exit -F arch=b64 -C euid!=uid -F euid=0 -Fauid>=1000 -F auid!=4294967295 -S execve -k actions
# CIS 4.1.16 — Kernel module loading
-w /sbin/insmod -p x -k modules
-w /sbin/rmmod -p x -k modules
-w /sbin/modprobe -p x -k modules
-a always,exit -F arch=b64 -S init_module -S delete_module -k modules
# CIS 4.1.17 — Immutable (MUST BE LAST)
-e 2
EOF
sudo augenrules --load
sudo systemctl enable --now auditd
5. Account & Access Management¶
CIS Controls: CIS 5.1 · CIS 5.2 · CIS 5.4
5.1 Create cloudadmin Account¶
sudo useradd -m -s /bin/bash -c "Cloud Infrastructure Admin" cloudadmin
sudo passwd cloudadmin # Set strong 20+ char password
# Grant sudo
echo "cloudadmin ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/cloudadmin
sudo chmod 440 /etc/sudoers.d/cloudadmin
5.2 Deploy SSH Public Key¶
sudo mkdir -p /home/cloudadmin/.ssh
sudo chmod 700 /home/cloudadmin/.ssh
sudo tee /home/cloudadmin/.ssh/authorized_keys << 'EOF'
<YOUR-CLOUDADMIN-ED25519-PUBLIC-KEY>
EOF
sudo chmod 600 /home/cloudadmin/.ssh/authorized_keys
sudo chown -R cloudadmin:cloudadmin /home/cloudadmin/.ssh
5.3 Disable Default Accounts¶
# Lock GCP default accounts if present
for acct in gce-obs-sa packer chronos; do
id "$acct" &>/dev/null && sudo usermod -L "$acct" && sudo usermod -s /sbin/nologin "$acct"
done
6. Firewall Configuration¶
CIS Controls: CIS 4.4 · CIS 12.1 · CIS 12.4
6.1 Base Firewall (all three VMs)¶
sudo systemctl enable --now firewalld
sudo firewall-cmd --set-default-zone=drop
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="10.8.0.0/28" port port="22" protocol="tcp" accept'
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="172.16.0.0/24" port port="22" protocol="tcp" accept'
sudo firewall-cmd --reload
6.2 OAK-specific Rules¶
# node_exporter — MAPLE scrape only
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="172.16.0.12" port port="9100" protocol="tcp" accept'
# Wazuh agent → MAPLE
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="172.16.0.12" port port="1514" protocol="tcp" accept'
sudo firewall-cmd --reload
6.3 MAPLE-specific Rules¶
# Prometheus UI — VPN management subnet + Cloud Run VPC connector
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="10.8.0.0/28" port port="9090" protocol="tcp" accept'
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="172.16.0.0/24" port port="9090" protocol="tcp" accept'
# Grafana — VPN management subnet
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="10.8.0.0/28" port port="3000" protocol="tcp" accept'
# Webmin
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="10.8.0.0/28" port port="10000" protocol="tcp" accept'
# Wazuh Manager — receive agents from VPC + WDC VPN range
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="172.16.0.0/24" port port="1514" protocol="tcp" accept'
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="172.16.0.0/24" port port="1515" protocol="tcp" accept'
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="192.168.120.0/23" port port="1514" protocol="tcp" accept'
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="192.168.120.0/23" port port="1515" protocol="tcp" accept'
# node_exporter scrape — MAPLE self
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="172.16.0.12" port port="9100" protocol="tcp" accept'
sudo firewall-cmd --reload
6.4 CEDAR-specific Rules¶
# Logstash syslog input — all VPC + WDC VPN sources
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="172.16.0.0/24" port port="5140" protocol="tcp" accept'
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="192.168.120.0/23" port port="5140" protocol="tcp" accept'
# Logstash Wazuh/Filebeat input — MAPLE
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="172.16.0.12" port port="5141" protocol="tcp" accept'
# Kibana — VPN management subnet
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="10.8.0.0/28" port port="5601" protocol="tcp" accept'
# Elasticsearch — MAPLE (Wazuh/Filebeat) + Cloud Run VPC connector (SOC backend)
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="172.16.0.12" port port="9200" protocol="tcp" accept'
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="10.8.0.0/28" port port="9200" protocol="tcp" accept'
# Webmin
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="10.8.0.0/28" port port="10000" protocol="tcp" accept'
# node_exporter — MAPLE scrape
sudo firewall-cmd --permanent --zone=drop --add-rich-rule='rule family="ipv4" source address="172.16.0.12" port port="9100" protocol="tcp" accept'
sudo firewall-cmd --reload
7. Disk Layout — Second Persistent Disk (LVM)¶
The second disk (sdb, 300 GB) must be partitioned with LVM matching the SKY/RAIN/SUN/WIND baseline. Adapt the LV sizes per the table in §1.3.
7.1 Initialize LVM on sdb¶
# Identify the second disk
lsblk # should show sdb with no partitions
# Create physical volume
sudo pvcreate /dev/sdb
# Create volume group
sudo vgcreate data /dev/sdb
7.2 OAK — Create Logical Volumes¶
sudo lvcreate -L 40G -n var_log data
sudo lvcreate -L 30G -n var_log_audit data
sudo lvcreate -L 180G -n openvas data
sudo lvcreate -L 40G -n backup data
# Format
for lv in var_log var_log_audit openvas backup; do
sudo mkfs.xfs /dev/data/$lv
done
# Mount points
sudo mkdir -p /var/log/audit /var/lib/openvas /backup
# Add to /etc/fstab
cat >> /etc/fstab << 'EOF'
/dev/data/var_log /var/log xfs defaults,nodev 0 2
/dev/data/var_log_audit /var/log/audit xfs defaults,nodev 0 2
/dev/data/openvas /var/lib/openvas xfs defaults,nodev,nosuid 0 2
/dev/data/backup /backup xfs defaults,nodev,nosuid 0 2
EOF
sudo mount -a
7.3 MAPLE — Create Logical Volumes¶
sudo lvcreate -L 30G -n var_log data
sudo lvcreate -L 20G -n var_log_audit data
sudo lvcreate -L 180G -n prometheus data
sudo lvcreate -L 50G -n backup data
for lv in var_log var_log_audit prometheus backup; do
sudo mkfs.xfs /dev/data/$lv
done
sudo mkdir -p /var/log/audit /var/lib/prometheus /backup
cat >> /etc/fstab << 'EOF'
/dev/data/var_log /var/log xfs defaults,nodev 0 2
/dev/data/var_log_audit /var/log/audit xfs defaults,nodev 0 2
/dev/data/prometheus /var/lib/prometheus xfs defaults,nodev,nosuid 0 2
/dev/data/backup /backup xfs defaults,nodev,nosuid 0 2
EOF
sudo mount -a
7.4 CEDAR — Create Logical Volumes¶
sudo lvcreate -L 40G -n var_log data
sudo lvcreate -L 30G -n var_log_audit data
sudo lvcreate -L 180G -n elasticsearch data
sudo lvcreate -L 30G -n logstash data
sudo lvcreate -L 10G -n backup data
for lv in var_log var_log_audit elasticsearch logstash backup; do
sudo mkfs.xfs /dev/data/$lv
done
sudo mkdir -p /var/log/audit /var/lib/elasticsearch /var/lib/logstash /backup
cat >> /etc/fstab << 'EOF'
/dev/data/var_log /var/log xfs defaults,nodev 0 2
/dev/data/var_log_audit /var/log/audit xfs defaults,nodev 0 2
/dev/data/elasticsearch /var/lib/elasticsearch xfs defaults,nodev,nosuid 0 2
/dev/data/logstash /var/lib/logstash xfs defaults,nodev,nosuid 0 2
/dev/data/backup /backup xfs defaults,nodev,nosuid 0 2
EOF
sudo mount -a
8. Webmin GUI Management¶
Installed on MAPLE and CEDAR for admin access. OAK is CLI-only (Docker environment).
# MAPLE and CEDAR only
cat > /etc/yum.repos.d/webmin.repo << 'EOF'
[Webmin]
name=Webmin Distribution Neutral
baseurl=https://download.webmin.com/download/yum
enabled=1
gpgcheck=1
gpgkey=https://download.webmin.com/jcameron-key.asc
EOF
sudo dnf install -y webmin
sudo systemctl enable --now webmin
# Restrict to cloudadmin only
sudo /usr/libexec/webmin/changepass.pl /etc/webmin root <strongpassword>
# Access: https://172.16.0.12:10000 (MAPLE) or https://172.16.0.13:10000 (CEDAR)
# Requires VPN management tunnel active
9. OAK — Docker CE + Greenbone/OpenVAS¶
CIS Controls: CIS 7.1 (Vulnerability Management)
9.1 Install Docker CE¶
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
# Add cloudadmin to docker group
sudo usermod -aG docker cloudadmin
9.2 Pull and Run Greenbone Community Container¶
# Create compose file
mkdir -p ~/greenbone && cd ~/greenbone
cat > docker-compose.yml << 'EOF'
---
services:
vulnerability-tests:
image: greenbone/vulnerability-tests
environment:
STORAGE_PATH: /var/lib/openvas/22.04/vt-data/nasl
volumes:
- vt_data_vol:/mnt
notus-data:
image: greenbone/notus-data
volumes:
- notus_data_vol:/mnt
scap-data:
image: greenbone/scap-data
volumes:
- scap_data_vol:/mnt
cert-bund-data:
image: greenbone/cert-bund-data
volumes:
- cert_bund_data_vol:/mnt
dfn-cert-data:
image: greenbone/dfn-cert-data
volumes:
- dfn_cert_data_vol:/mnt
depends_on:
- cert-bund-data
data-objects:
image: greenbone/data-objects
volumes:
- data_objects_vol:/mnt
report-formats:
image: greenbone/report-formats
volumes:
- report_formats_vol:/mnt
depends_on:
- data-objects
gpg-data:
image: greenbone/gpg-data
volumes:
- gpg_data_vol:/mnt
redis-server:
image: greenbone/redis-server
restart: on-failure
volumes:
- redis_socket_vol:/run/redis/
pg-gvm:
image: greenbone/pg-gvm:stable
restart: on-failure
volumes:
- psql_data_vol:/var/lib/postgresql
- psql_socket_vol:/var/run/postgresql
gvmd:
image: greenbone/gvmd:stable
restart: on-failure
volumes:
- gvmd_data_vol:/var/lib/gvm
- scap_data_vol:/var/lib/gvm/scap-data/
- cert_bund_data_vol:/var/lib/gvm/cert-data
- data_objects_vol:/var/lib/gvm/data-objects/gvmd
- vt_data_vol:/var/lib/openvas/plugins
- psql_data_vol:/var/lib/postgresql
- gvmd_socket_vol:/run/gvmd
- ospd_openvas_socket_vol:/run/ospd
- notus_data_vol:/notus
- psql_socket_vol:/var/run/postgresql
- gpg_data_vol:/etc/openvas/gnupg
- report_formats_vol:/var/lib/gvm/data-objects/gvmd/22.04/report-formats
depends_on:
pg-gvm:
condition: service_started
gsad:
image: greenbone/gsad:stable
restart: on-failure
ports:
- 127.0.0.1:9390:80
volumes:
- gvmd_socket_vol:/run/gvmd
ospd-openvas:
image: greenbone/ospd-openvas:stable
restart: on-failure
init: true
cap_add:
- NET_ADMIN
- NET_RAW
security_opt:
- seccomp=unconfined
- apparmor=unconfined
command:
[
"ospd-openvas",
"-f",
"--config", "/etc/gvm/ospd-openvas.conf",
"--notus-feed-dir", "/notus/advisories",
"-m", "666"
]
volumes:
- gpg_data_vol:/etc/openvas/gnupg
- vt_data_vol:/var/lib/openvas/plugins
- notus_data_vol:/notus
- ospd_openvas_socket_vol:/run/ospd
- redis_socket_vol:/run/redis/
notus-scanner:
image: greenbone/notus-scanner:stable
restart: on-failure
volumes:
- notus_data_vol:/notus
- ospd_openvas_socket_vol:/run/ospd
environment:
DISABLE_NOTUS_HASHSUM_VERIFICATION: "True"
depends_on:
- redis-server
- ospd-openvas
gvm-tools:
image: greenbone/gvm-tools
volumes:
- gvmd_socket_vol:/run/gvmd
- ospd_openvas_socket_vol:/run/ospd
depends_on:
- gvmd
- gsad
volumes:
psql_data_vol:
psql_socket_vol:
gvmd_data_vol:
vt_data_vol:
notus_data_vol:
ospd_openvas_socket_vol:
redis_socket_vol:
gvmd_socket_vol:
scap_data_vol:
cert_bund_data_vol:
dfn_cert_data_vol:
data_objects_vol:
report_formats_vol:
gpg_data_vol:
EOF
# Start Greenbone
docker compose -f ~/greenbone/docker-compose.yml -p greenbone-community pull
docker compose -f ~/greenbone/docker-compose.yml -p greenbone-community up -d
9.3 Initial Feed Sync (allow 15–30 min)¶
docker compose -f ~/greenbone/docker-compose.yml -p greenbone-community \
exec -u gvmd gvmd gvmd --rebuild
9.4 Set Admin Password¶
docker compose -f ~/greenbone/docker-compose.yml -p greenbone-community \
exec -u gvmd gvmd gvmd --user=admin --new-password='<STRONG-PASSWORD>'
9.5 Configure Weekly Scan — Sunday 01:00¶
After initial feed sync, log into OpenVAS via SSH tunnel and configure a scan task covering all WDC + GCP IPs:
# SSH tunnel to access OpenVAS UI locally
ssh -L 9390:127.0.0.1:9390 cloudadmin@172.16.0.10
# Open in browser: http://localhost:9390
# Credentials: admin / <password set above>
Target IPs to configure in the scan task:
# WDC On-Prem (over VPN)
192.168.120.1 # SKY
192.168.120.2 # RAIN
192.168.120.3 # SUN
192.168.120.4 # WIND
# GCP VMs (same VPC)
172.16.0.10 # OAK (self)
172.16.0.12 # MAPLE
172.16.0.13 # CEDAR
Schedule: Weekly, Sunday 01:00 UTC
9.6 Autostart on Boot¶
cat > /etc/systemd/system/greenbone.service << 'EOF'
[Unit]
Description=Greenbone Community Container Stack
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/root/greenbone
ExecStart=/usr/bin/docker compose -p greenbone-community up -d
ExecStop=/usr/bin/docker compose -p greenbone-community down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now greenbone
9.7 OpenVAS Auto-Export — /usr/local/bin/gpus-openvas-export.sh¶
CIS Controls: CIS 7 (Continuous Vulnerability Management) · CIS 11 (Data Recovery) · CIS 13 (Network Monitoring & Defense)
The SOC dashboard's Vulnerability Scan tab consumes OpenVAS findings from a single GCS object, gs://gpus-infra-backups-wdc/security/openvas/latest.json. To decouple the dashboard from OAK's availability and to give the SOC backend a stable, versioned feed, OAK runs a daily export script that extracts the latest Greenbone scan report, normalizes it to the dashboard's JSON schema, and uploads it to GCS via the instance service account.
Script location: /usr/local/bin/gpus-openvas-export.sh (owner root, mode 0755).
Upload target: gs://gpus-infra-backups-wdc/security/openvas/latest.json (overwritten on each run; previous versions retained under security/openvas/archive/YYYY-MM-DD.json).
Execution strategy. The script is designed to be resilient to Greenbone container restarts and feed-sync windows. It tries three collection modes in order and writes a stub findings file rather than failing hard:
- GMP socket via
socat. The preferred path.gvm-cliis invoked against the gvmd UNIX socket exposed by the container (/var/run/gvmd/gvmd.sock), with asocatbridge translating the Docker volume-mounted socket into a local stream thatgvm-clican read. This mode produces a complete report with host, port, CVSS, and severity fields. - Container-side
gvmdCLI. Fallback when the socket bridge fails (e.g. the gvmd container is still warming up after feed rebuild). The scriptdocker execs into thegreenbone-community-gvmd-1container and callsgvmd --get-reports --verbosedirectly. - Manual stub with empty findings. Final fallback when both of the above fail. The script writes a well-formed JSON document with
findings: []and asource: "stub"marker so the SOC backend's_normalize_openvas()still returns a safe payload rather than surfacing an error on the Vulnerability Scan tab.
sudo tee /usr/local/bin/gpus-openvas-export.sh >/dev/null << 'SCRIPT'
#!/usr/bin/env bash
# gpus-openvas-export.sh — Export latest OpenVAS report to GCS
# Owner: root Mode: 0755
# Ref: cloud-vms.md §9.7
set -eo pipefail
BUCKET="gs://gpus-infra-backups-wdc/security/openvas"
TARGET="${BUCKET}/latest.json"
ARCHIVE="${BUCKET}/archive/$(date +%F).json"
TMP="$(mktemp --suffix=.json)"
trap 'rm -f "$TMP"' EXIT
emit_stub() {
cat > "$TMP" <<JSON
{
"source": "stub",
"generated_at": "$(date -u +%FT%TZ)",
"host": "$(hostname -s)",
"findings": [],
"error": "${1:-no data}"
}
JSON
}
# --- Mode 1: GMP over socat bridge to gvmd socket ---
if command -v gvm-cli >/dev/null && command -v socat >/dev/null; then
SOCK=/run/gvmd/gvmd.sock
if docker exec greenbone-community-gvmd-1 test -S "$SOCK" 2>/dev/null; then
gvm-cli socket --socketpath "$SOCK" --xml "<get_reports details=\"1\"/>" \
| /usr/local/bin/gpus-openvas-parse.py > "$TMP" && MODE=gmp
fi
fi
# --- Mode 2: gvmd CLI inside the container ---
if [[ -z "${MODE:-}" ]]; then
if docker exec greenbone-community-gvmd-1 gvmd --get-reports --verbose 2>/dev/null \
| /usr/local/bin/gpus-openvas-parse.py > "$TMP"; then
MODE=container
fi
fi
# --- Mode 3: stub ---
if [[ -z "${MODE:-}" ]] || [[ ! -s "$TMP" ]]; then
emit_stub "no export mode succeeded"
MODE=stub
fi
logger -t gpus-openvas-export "export mode=${MODE} bytes=$(wc -c < "$TMP")"
# Upload to GCS via instance service account
gcloud storage cp "$TMP" "$TARGET"
gcloud storage cp "$TMP" "$ARCHIVE"
SCRIPT
sudo chmod 0755 /usr/local/bin/gpus-openvas-export.sh
socat installation (required for Mode 1):
Daily schedule — systemd timer:
sudo tee /etc/systemd/system/gpus-openvas-export.service >/dev/null << 'EOF'
[Unit]
Description=GPUS OpenVAS → GCS export
After=docker.service greenbone.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/gpus-openvas-export.sh
EOF
sudo tee /etc/systemd/system/gpus-openvas-export.timer >/dev/null << 'EOF'
[Unit]
Description=Run gpus-openvas-export daily
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now gpus-openvas-export.timer
Duplicate Greenbone Container Names
The Greenbone Community Docker stack on OAK has been seen to expose the same service under two different Docker project names depending on how the compose file was started:
| Project name | Container examples | Origin |
|---|---|---|
greenbone-community-gvmd-1 |
greenbone-community-gvmd-1, greenbone-community-ospd-openvas-1, greenbone-community-gsad-1 |
docker compose -p greenbone-community up -d (our convention — see §9.6 systemd unit) |
greenbone-community-edition-gvmd-1 |
greenbone-community-edition-gvmd-1, etc. |
Default project name when the compose command is run from a directory called greenbone-community-edition/ (the upstream Greenbone docs' suggested clone path) |
Both stacks share the same named volumes (gvmd_data_vol, vt_data_vol, etc.), so starting one after the other produces a conflicting, half-running state: the older stack still holds file locks and the newer one can't take them.
The export script assumes the -p greenbone-community naming (container greenbone-community-gvmd-1) per our systemd unit. If you see the -edition- variant running in docker ps, stop it with docker compose -p greenbone-community-edition down before the export script will succeed.
To check for duplication:
Manual verification:
# Run the export on demand
sudo /usr/local/bin/gpus-openvas-export.sh
# Confirm GCS has the fresh object
gcloud storage ls -L gs://gpus-infra-backups-wdc/security/openvas/latest.json \
| grep -E 'Creation time|Content-Length'
# Tail the logger output
sudo journalctl -t gpus-openvas-export -n 20
SOC backend consumption. soc-backend/app.py::get_openvas_findings() reads latest.json from GCS and runs it through _normalize_openvas(), which guarantees a findings: [] array regardless of which export mode produced the file. This is what makes the "7 findings" Vulnerability Scan table on the dashboard tolerant of OAK outages — even a stub payload renders a clean empty-state row rather than crashing the tab. See SOC Dashboard — Vulnerability Scan Findings Table for the frontend side of the feed.
10. MAPLE — Prometheus + Grafana + Wazuh Manager¶
10.1 Install Prometheus¶
PROM_VER="2.51.0"
cd /tmp
curl -LO https://github.com/prometheus/prometheus/releases/download/v${PROM_VER}/prometheus-${PROM_VER}.linux-amd64.tar.gz
tar xzf prometheus-${PROM_VER}.linux-amd64.tar.gz
sudo useradd -rs /sbin/nologin prometheus
sudo mv prometheus-${PROM_VER}.linux-amd64/{prometheus,promtool} /usr/local/bin/
sudo mkdir -p /etc/prometheus /var/lib/prometheus
sudo mv prometheus-${PROM_VER}.linux-amd64/{consoles,console_libraries,prometheus.yml} /etc/prometheus/
sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus
10.2 prometheus.yml — Scrape All 7 VMs¶
# /etc/prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# WDC On-Prem (over VPN)
- job_name: 'wdc-sky'
static_configs:
- targets: ['192.168.120.1:9100']
labels: {server: 'sky', site: 'wdc', role: 'dns-dhcp'}
- job_name: 'wdc-rain'
static_configs:
- targets: ['192.168.120.2:9100']
labels: {server: 'rain', site: 'wdc', role: 'dns-dhcp'}
- job_name: 'wdc-sun'
static_configs:
- targets: ['192.168.120.3:9100']
labels: {server: 'sun', site: 'wdc', role: 'monitoring'}
- job_name: 'wdc-wind'
static_configs:
- targets: ['192.168.120.4:9100']
labels: {server: 'wind', site: 'wdc', role: 'logging'}
# GCP Cloud VMs
- job_name: 'cloud-oak'
static_configs:
- targets: ['172.16.0.10:9100']
labels: {server: 'oak', site: 'gcp', role: 'scanner'}
- job_name: 'cloud-maple'
static_configs:
- targets: ['localhost:9100']
labels: {server: 'maple', site: 'gcp', role: 'monitoring'}
- job_name: 'cloud-cedar'
static_configs:
- targets: ['172.16.0.13:9100']
labels: {server: 'cedar', site: 'gcp', role: 'logging'}
cat > /etc/systemd/system/prometheus.service << 'EOF'
[Unit]
Description=Prometheus
After=network.target
[Service]
Type=simple
User=prometheus
ExecStart=/usr/local/bin/prometheus \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/var/lib/prometheus \
--storage.tsdb.retention.time=90d \
--web.listen-address=0.0.0.0:9090
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now prometheus
10.3 Install Grafana¶
cat > /etc/yum.repos.d/grafana.repo << 'EOF'
[grafana]
name=grafana
baseurl=https://packages.grafana.com/oss/rpm
repo_gpgcheck=1
enabled=1
gpgcheck=1
gpgkey=https://packages.grafana.com/gpg.key
EOF
sudo dnf install -y grafana
sudo systemctl enable --now grafana-server
# Import dashboards (ID 1860 — Node Exporter Full, ID 13955 — BIND DNS)
# Access: http://172.16.0.12:3000 (admin/admin → change on first login)
10.4 Install Wazuh Manager¶
# Import Wazuh GPG key and repo
rpm --import https://packages.wazuh.com/key/GPG-KEY-WAZUH
cat > /etc/yum.repos.d/wazuh.repo << 'EOF'
[wazuh]
gpgcheck=1
gpgkey=https://packages.wazuh.com/key/GPG-KEY-WAZUH
enabled=1
name=EL-$releasever - Wazuh
baseurl=https://packages.wazuh.com/4.x/yum/
protect=1
EOF
sudo dnf install -y wazuh-manager
sudo systemctl enable --now wazuh-manager
# Verify
sudo systemctl status wazuh-manager
# Port check
sudo ss -tlnp | grep wazuh
10.5 Custom Wazuh LOLBin Detection Rules (MAPLE)¶
Custom rules for LOLBin attack detection, deployed at /var/ossec/etc/rules/gpus-lolbin-rules.xml on MAPLE. These rules were created and validated during the BT-001/BT-002 drills (see BT-001 LOLBin Drill).
<!-- /var/ossec/etc/rules/gpus-lolbin-rules.xml -->
<group name="gpus-lolbin,">
<!-- Rule 100010: T1105 — curl/wget to external URL (Level 10 High) -->
<rule id="100010" level="10">
<if_sid>80700</if_sid>
<match>curl|wget</match>
<description>LOLBin: curl/wget external transfer detected (T1105)</description>
<mitre><id>T1105</id></mitre>
<group>gpus-lolbin,ingress_tool_transfer,</group>
</rule>
<!-- Rule 100011: T1027/T1140 — base64 encode/decode (Level 10 High) -->
<rule id="100011" level="10">
<if_sid>80700</if_sid>
<match>base64</match>
<description>LOLBin: base64 encode/decode operation detected (T1027/T1140)</description>
<mitre><id>T1027</id><id>T1140</id></mitre>
<group>gpus-lolbin,obfuscation,</group>
</rule>
<!-- Rule 100012: T1053.003 — crontab persistence (Level 12 Critical) -->
<!-- Tuned: requires shell/reverse-shell patterns to reduce false positives -->
<rule id="100012" level="12">
<if_sid>80700</if_sid>
<match>crontab</match>
<regex>bash|sh|nc|ncat|reverse|shell|python|perl</regex>
<description>LOLBin: suspicious crontab persistence detected (T1053.003)</description>
<mitre><id>T1053.003</id></mitre>
<group>gpus-lolbin,persistence,</group>
</rule>
<!-- Rule 100013: T1003 — /etc/shadow access by non-root (Level 12 Critical) -->
<rule id="100013" level="12">
<if_sid>80700</if_sid>
<match>/etc/shadow</match>
<description>LOLBin: /etc/shadow access attempt detected (T1003)</description>
<mitre><id>T1003</id></mitre>
<group>gpus-lolbin,credential_access,</group>
</rule>
</group>
After deploying rules, restart the Wazuh Manager:
10.6 Install Filebeat (Wazuh Alert Forwarding)¶
Filebeat forwards Wazuh alerts from the Manager on MAPLE to CEDAR's Elasticsearch, writing to the wazuh-alerts-* index pattern.
sudo dnf install -y filebeat
# Configure Filebeat for Wazuh alerts
cat > /etc/filebeat/filebeat.yml << 'EOF'
filebeat.modules:
- module: wazuh
alerts:
enabled: true
var.paths: ["/var/ossec/logs/alerts/alerts.json"]
output.elasticsearch:
hosts: ["172.16.0.13:9200"]
index: "wazuh-alerts-%{+yyyy.MM.dd}"
setup.template.name: "wazuh-alerts"
setup.template.pattern: "wazuh-alerts-*"
setup.ilm.enabled: false
EOF
sudo systemctl enable --now filebeat
# Verify alerts are reaching CEDAR
# curl -s 172.16.0.13:9200/wazuh-alerts-*/_count | jq .count
11. CEDAR — ELK Stack + Wazuh Indexer¶
11.1 Install Elasticsearch¶
cat > /etc/yum.repos.d/elasticsearch.repo << 'EOF'
[elasticsearch]
name=Elasticsearch repository for 8.x packages
baseurl=https://artifacts.elastic.co/packages/8.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=0
autorefresh=1
type=rpm-md
EOF
sudo dnf install --enablerepo=elasticsearch -y elasticsearch
# Configure
sudo sed -i 's/#network.host:.*/network.host: localhost/' /etc/elasticsearch/elasticsearch.yml
sudo sed -i 's/#http.port:.*/http.port: 9200/' /etc/elasticsearch/elasticsearch.yml
echo "path.data: /var/lib/elasticsearch" | sudo tee -a /etc/elasticsearch/elasticsearch.yml
echo "path.logs: /var/log/elasticsearch" | sudo tee -a /etc/elasticsearch/elasticsearch.yml
sudo systemctl enable --now elasticsearch
11.2 Install Logstash¶
sudo dnf install --enablerepo=elasticsearch -y logstash
cat > /etc/logstash/conf.d/01-gpus-syslog.conf << 'EOF'
input {
syslog {
port => 5140
type => "syslog"
}
}
filter {
if [type] == "syslog" {
grok {
match => { "message" => "%{SYSLOGTIMESTAMP:syslog_timestamp} %{SYSLOGHOST:syslog_host} %{DATA:syslog_program}(?:\[%{POSINT:syslog_pid}\])?: %{GREEDYDATA:syslog_message}" }
}
date {
match => [ "syslog_timestamp", "MMM d HH:mm:ss", "MMM dd HH:mm:ss" ]
}
mutate {
add_tag => [ "%{syslog_host}", "%{syslog_program}" ]
}
}
}
output {
elasticsearch {
hosts => ["localhost:9200"]
index => "cloud-logs-%{+YYYY.MM.dd}"
}
}
EOF
sudo systemctl enable --now logstash
11.2.1 Logstash Wazuh Pipeline (CEDAR)¶
A second Logstash pipeline receives Wazuh alerts forwarded by Filebeat from MAPLE and indexes them into CEDAR Elasticsearch under the wazuh-alerts-* pattern.
cat > /etc/logstash/conf.d/02-wazuh-alerts.conf << 'EOF'
input {
beats {
port => 5141
type => "wazuh"
}
}
filter {
if [type] == "wazuh" {
json {
source => "message"
}
date {
match => [ "timestamp", "ISO8601" ]
}
}
}
output {
if [type] == "wazuh" {
elasticsearch {
hosts => ["localhost:9200"]
index => "wazuh-alerts-%{+YYYY.MM.dd}"
}
}
}
EOF
sudo systemctl restart logstash
11.3 Install Kibana¶
sudo dnf install --enablerepo=elasticsearch -y kibana
sudo sed -i 's/#server.host:.*/server.host: "0.0.0.0"/' /etc/kibana/kibana.yml
sudo sed -i 's/#elasticsearch.hosts:.*/elasticsearch.hosts: ["http:\/\/localhost:9200"]/' /etc/kibana/kibana.yml
sudo systemctl enable --now kibana
11.4 Install Wazuh Indexer¶
# Wazuh indexer runs alongside Elasticsearch, using the same ES instance
# Install the Wazuh Kibana plugin for SIEM dashboards
sudo -u kibana /usr/share/kibana/bin/kibana-plugin install \
https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.x.x_7.x.x-1.zip
sudo systemctl restart kibana
12. Wazuh Agents — All 7 Servers¶
Run on each server (SKY, RAIN, SUN, WIND, OAK, MAPLE, CEDAR). All agents point to MAPLE (172.16.0.12).
# Add Wazuh repo (same as MAPLE step)
rpm --import https://packages.wazuh.com/key/GPG-KEY-WAZUH
cat > /etc/yum.repos.d/wazuh.repo << 'EOF'
[wazuh]
gpgcheck=1
gpgkey=https://packages.wazuh.com/key/GPG-KEY-WAZUH
enabled=1
name=EL-$releasever - Wazuh
baseurl=https://packages.wazuh.com/4.x/yum/
protect=1
EOF
sudo dnf install -y wazuh-agent
# Configure agent → MAPLE manager
sudo sed -i 's/<address>MANAGER_IP<\/address>/<address>172.16.0.12<\/address>/' \
/var/ossec/etc/ossec.conf
sudo systemctl enable --now wazuh-agent
# Verify connection (check on MAPLE)
# sudo /var/ossec/bin/agent_control -la
13. Prometheus Exporters — All Cloud VMs¶
# Install node_exporter on OAK, MAPLE, CEDAR
NODE_VER="1.8.0"
cd /tmp
curl -LO https://github.com/prometheus/node_exporter/releases/download/v${NODE_VER}/node_exporter-${NODE_VER}.linux-amd64.tar.gz
tar xzf node_exporter-${NODE_VER}.linux-amd64.tar.gz
sudo useradd -rs /sbin/nologin node_exporter
sudo mv node_exporter-${NODE_VER}.linux-amd64/node_exporter /usr/local/bin/
cat > /etc/systemd/system/node_exporter.service << 'EOF'
[Unit]
Description=Node Exporter
After=network.target
[Service]
User=node_exporter
ExecStart=/usr/local/bin/node_exporter
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now node_exporter
14. Log Forwarding — All VMs to CEDAR¶
Add to each VM's rsyslog to forward to CEDAR:5140.
cat > /etc/rsyslog.d/50-forward-cedar.conf << 'EOF'
# Forward all logs to CEDAR Logstash — CIS 8.9
$ActionQueueType LinkedList
$ActionQueueFileName fwdRule1
$ActionQueueMaxDiskSpace 1g
$ActionQueueSaveOnShutdown on
$ActionResumeRetryCount -1
*.* @@172.16.0.13:5140
EOF
sudo systemctl restart rsyslog
15. Backup Configuration — GCS via Instance SA¶
All GCP VMs use their instance service accounts for GCS access. No key file required.
# Install gcloud SDK (if not present on GCP image)
sudo dnf install -y google-cloud-sdk
# Test access — instance SA has storage.objectAdmin on gpus-infra-backups-wdc
gsutil ls gs://gpus-infra-backups-wdc/
# Install backup script (same as WDC servers but uses instance SA — no key file)
sudo cp /path/to/gpus-backup.sh /usr/local/bin/gpus-backup.sh
sudo chmod 750 /usr/local/bin/gpus-backup.sh
# Set up daily cron
echo "0 2 * * * cloudadmin /usr/local/bin/gpus-backup.sh >> /var/log/gpus-backup.log 2>&1" \
| sudo tee /etc/cron.d/gpus-backup
16. File Integrity Monitoring — AIDE¶
sudo dnf install -y aide
# Exclude dynamic GCP/Docker paths
cat >> /etc/aide.conf << 'EOF'
!/proc
!/sys
!/run
!/var/lib/docker
!/tmp
!/var/tmp
EOF
# Initialize baseline
sudo aide --init
sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
# Daily cron
echo "0 3 * * * root /usr/sbin/aide --check >> /var/log/aide/aide.log 2>&1" \
| sudo tee /etc/cron.d/aide
sudo mkdir -p /var/log/aide
16a. Lynis — System Hardening Audits (All 3 Cloud VMs)¶
CIS Controls: CIS 4.1 · CIS 4.8
Lynis is installed on all three cloud VMs (OAK, MAPLE, CEDAR) for automated system hardening audits. Scores are collected by the SOC backend via SSH.
# Install Lynis on each cloud VM
sudo dnf install -y lynis
# Run initial audit
sudo lynis audit system --no-colors > /var/log/lynis-report.dat 2>&1
# Weekly cron for automated audits
echo "0 4 * * 0 root /usr/bin/lynis audit system --no-colors --cronjob > /var/log/lynis-report.dat 2>&1" \
| sudo tee /etc/cron.d/lynis
16b. auditd LOLBin Rules (All 3 Cloud VMs)¶
Status: Rules deployed, pending reboot to activate immutable mode.
Custom auditd rules for LOLBin detection, complementing the Wazuh custom rules on MAPLE. These rules generate syscall-level audit events that Wazuh agents pick up and forward.
cat >> /etc/audit/rules.d/gpus-lolbin.rules << 'EOF'
# LOLBin detection — curl/wget/base64 execution
-w /usr/bin/curl -p x -k gpus-lolbin-t1105
-w /usr/bin/wget -p x -k gpus-lolbin-t1105
-w /usr/bin/base64 -p x -k gpus-lolbin-t1027
# Shadow file access
-w /etc/shadow -p r -k gpus-lolbin-t1003
# Crontab modifications
-w /usr/bin/crontab -p x -k gpus-lolbin-t1053
-w /var/spool/cron -p wa -k gpus-lolbin-t1053
EOF
# Load rules (requires reboot if auditd is in immutable mode -e 2)
sudo augenrules --load
# If immutable: sudo reboot
17. DNS Registration — cloud.us zone¶
Run on SKY (192.168.120.1). Then re-sign DNSSEC and update RAIN via zone transfer.
# Edit cloud.us zone file on SKY
sudo vim /var/named/cloud.us.db
# Add records (if not already present):
# oak IN A 172.16.0.10
# maple IN A 172.16.0.12
# cedar IN A 172.16.0.13
# Increment serial (YYYYMMDDNN format)
# Reload
sudo rndc reload cloud.us
# Re-sign DNSSEC
sudo dnssec-signzone -A -3 $(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n') \
-N INCREMENT -o cloud.us -t /var/named/cloud.us.db
# Update AIDE baseline on SKY after zone change
sudo aide --update && sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
# Log change
echo "$(date '+%Y-%m-%d %H:%M') - Added DNS records: oak/maple/cedar to cloud.us zone" \
| sudo tee -a /var/log/asset-inventory.log
# RAIN will receive zone transfer automatically via AXFR notify
18. Status Backend Integration¶
Add OAK, MAPLE, CEDAR to
app.pySERVERS dict and to the status site HTML.
18.1 app.py additions¶
# Add to SERVERS dict in app.py:
SERVERS = {
# ... existing WDC servers ...
"OAK": {"ip": "172.16.0.10", "user": "cloudadmin", "services": ["docker","fail2ban","auditd","firewalld"]},
"MAPLE": {"ip": "172.16.0.12", "user": "cloudadmin", "services": ["prometheus","grafana-server","wazuh-manager","fail2ban","auditd","firewalld"]},
"CEDAR": {"ip": "172.16.0.13", "user": "cloudadmin", "services": ["elasticsearch","logstash","kibana","fail2ban","auditd","firewalld"]},
}
Add cloudadmin SSH key to status backend and open port 22 from 10.8.0.0/28 on OAK/MAPLE/CEDAR firewalls (done in §6).
19. Verification & Testing¶
19.1 VPN Connectivity¶
# From OAK/MAPLE/CEDAR — verify WDC servers reachable
ping -c 3 192.168.120.1 # SKY
ping -c 3 192.168.120.3 # SUN
dig oak.cloud.us @192.168.120.1
19.2 Prometheus Scrape (MAPLE)¶
curl -s http://localhost:9090/api/v1/targets | python3 -m json.tool | grep health
# All 7 targets should show "health": "up"
19.3 Log Forwarding (CEDAR)¶
# Check Logstash listening
sudo ss -tlnp | grep 5140
# Check ES received logs
curl -s http://localhost:9200/_cat/indices?v | grep cloud-logs
19.4 OpenVAS (OAK)¶
# Check containers running
docker compose -f ~/greenbone/docker-compose.yml -p greenbone-community ps
# Access UI via tunnel
ssh -L 9390:127.0.0.1:9390 cloudadmin@172.16.0.10
# Open http://localhost:9390
19.5 Wazuh Agents (MAPLE)¶
20. Post-Change Checklist¶
NEVER SKIP after any configuration change on these servers.
# 1. Update AIDE baseline
sudo aide --update && sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
# 2. Log change to asset inventory
echo "$(date '+%Y-%m-%d %H:%M') - <description of change>" \
| sudo tee -a /var/log/asset-inventory.log
# 3. Re-sign DNSSEC if cloud.us zone files changed (run on SKY)
# sudo dnssec-signzone ... (see §17)
Appendix — Quick Reference¶
| Item | OAK | MAPLE | CEDAR |
|---|---|---|---|
| IP | 172.16.0.10 | 172.16.0.12 | 172.16.0.13 |
| FQDN | oak.cloud.us | maple.cloud.us | cedar.cloud.us |
| Admin | cloudadmin | cloudadmin | cloudadmin |
| Webmin | — | https://172.16.0.12:10000 | https://172.16.0.13:10000 |
| Prometheus | — | http://172.16.0.12:9090 | — |
| Grafana | — | http://172.16.0.12:3000 | — |
| Kibana | — | — | http://172.16.0.13:5601 |
| OpenVAS | http://localhost:9390 (tunnel) | — | — |
| Service Account | oak-sa@gpus-infra | maple-sa@gpus-infra | cedar-sa@gpus-infra |
| Machine Type | e2-standard-2 | e2-standard-2 | e2-standard-4 |
| Boot Disk | 200 GB SSD | 200 GB SSD | 200 GB SSD |
| Data Disk | 300 GB SSD | 300 GB SSD | 300 GB SSD |
OAK / MAPLE / CEDAR — GCP Cloud Server Deployment Guide
Project: gpus-infra · Region: us-central1 · VPC: 172.16.0.0/24
CIS Rocky Linux 8 hardened · Terraform managed · VPN-connected to WDC
Classification: CONFIDENTIAL — Internal Use Only