Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Wazuh/node_modules/
Wazuh/package-lock.json
Wazuh/package.json
Wazuh/.env
Wazuh/.gitignore

21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This repository contains configuration files and scripts for managing a Proxmox-
## Cluster Graph

```mermaid

graph TD
%% Repository Structure
REPO[opensource-mieweb Repository]
Expand Down Expand Up @@ -37,14 +37,21 @@ graph TD
USER[User Access] --> DNS
DNS --> NGINX
NGINX --> CONTAINER


%% Wazuh Integration
CONTAINER --> |Wazuh Agent| WAGENT[Wazuh Agent]
WAGENT --> |reports to| WMANAGER[Wazuh Manager]
WMANAGER --> |sends data to| WINDEXER[Wazuh Indexer]

%% Styling
classDef folder fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
classDef system fill:#f1f8e9,stroke:#689f38,stroke-width:2px
classDef wazuh fill:#fffde7,stroke:#fbc02d,stroke-width:2px
classDef user fill:#fff3e0,stroke:#f57c00,stroke-width:2px

class CICD,CC,DNS,GW,LDAP,NGINX,PL folder
class CONTAINER system
class WAGENT,WMANAGER,WINDEXER wazuh
class USER user
```

Expand Down Expand Up @@ -72,12 +79,17 @@ graph TD
- [`LDAP/`](LDAP/):
Contains LDAP authentication infrastructure including a custom Node.js LDAP server that bridges database user management with LDAP protocols, and automated LDAP client configuration tools for seamless container authentication integration. LDAP Server configured to reference the [Proxmox VE Users @pve realm](https://pve.proxmox.com/wiki/User_Management) with optional [Push Notification 2FA](https://github.com/mieweb/mieweb_auth_app)

### Security

- [`Wazuh/`](Wazuh/):
We utilize Wazuh, an opensource security management platform, to provide vulnerability detection and threat hunting services to our cluster. Our custom decoders and rules revolve mainly around mitigating SSH/PAM bruteforce attacks in both our hypervisors and individual containers.

### GitHub Action Integration

- [`proxmox-launchpad/`](proxmox-launchpad/):
The Proxmox LaunchPad GitHub Action for automated container deployment directly from GitHub repositories, supporting both single and multi-component applications.

- [A LDAPServer Server](https://github.com/mieweb/LDAPServer):
- [`LDAPServer`](https://github.com/mieweb/LDAPServer):
LDAP Server configured to reference the [Proxmox VE Users @pve realm](https://pve.proxmox.com/wiki/User_Management) with optional [Push Notification 2FA](https://github.com/mieweb/mieweb_auth_app)

## Create a Container
Expand All @@ -99,6 +111,7 @@ If you have an account in the [opensource-mieweb](https://opensource.mieweb.org:
- **GitHub Integration**: The Proxmox LaunchPad action automates the entire process from repository push to live deployment, including dependency installation, service configuration, and application startup.
- **CI/CD Pipeline**: Automated scripts used by [Proxmox LaunchPad](#proxmox-launchpad) to handle container updates, existence checks, and cleanup operations to maintain a clean and efficient hosting environment.
- **LDAP Server**: All LXC Container Authentication is handled by a centralized LDAP server housed in the cluster. Each Container is configured with SSSD, which communicates with the LDAP server to verify/authenitcate user credentials. This approach is more secure than housing credentials locally.
- **Wazuh**: Both containers and hypervisors are Wazuh Agents, and send all logs to our centralized Wazuh Manager, which matches each log against a large database of decoders and rules. If certain rules are triggered, active response mechanisms respond by triggering certain commands, a common one being a firewall drop of all packets originating from a certain source IP.


## Proxmox LaunchPad
Expand Down
86 changes: 86 additions & 0 deletions Wazuh/firewall-block.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/bin/bash
# Custom Firewall Drop Script for Wazuh
# Template Generated by Copilot
# Last Modified by Maxwell Klema on August 11th, 2025
# ---------------------------------------------------

# Usage: This script reads JSON input from STDIN
# Expected JSON format:
# {
# "version": 1,
# "command": "add|delete",
# "parameters": {
# "alert": {
# "data": {
# "srcip": "IP_ADDRESS"
# }
# }
# }
# }
# Full list: https://documentation.wazuh.com/current/user-manual/capabilities/active-response/custom-active-response-scripts.html

# Set script name for logging
LOG_FILE="/var/ossec/logs/active-responses.log"

# Function to log messages
log_message() {
echo "[$(date '+%Y/%m/%d %H:%M:%S')] $1" >> "$LOG_FILE"
}

# Read JSON input from STDIN
read -r INPUT

# Parse JSON to extract command and srcip
COMMAND=$(echo "$INPUT" | jq -r '.command // empty')
SRCIP=$(echo "$INPUT" | jq -r '.parameters.alert.data.srcip // empty')

# Validate input
if [[ -z "$COMMAND" || -z "$SRCIP" ]]; then
log_message "ERROR: Invalid input - missing command or srcip"
exit 1
fi

# Validate IP address format
if ! [[ "$SRCIP" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
log_message "ERROR: Invalid IP address format: $SRCIP"
exit 1
fi

# Function to add firewall rule
add_rule() {
local ip="$1"
ipset add blacklist "$ip" 2>/dev/null
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The big thing I wanted to point out that I forgot to mention was the move to ipset for better performance on a larger scope of IP's, but it looks like this was already changed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The big thing I wanted to point out that I forgot to mention was the move to ipset for better performance on a larger scope of IP's, but it looks like this was already changed.

Yes, it provides much better performance and cleaner code!

if [[ $? -eq 0 ]]; then
log_message "Successfully added $ip to blacklist"
else
log_message "WARNING: Failed to add $ip to blacklist or it may already exist"
fi
}

# Function to remove firewall rule
remove_rule() {
local ip="$1"

ipset del blacklist "$ip" 2>/dev/null
if [[ $? -eq 0 ]]; then
log_message "Successfully removed $ip from blacklist"
else
log_message "WARNING: Failed to remove $ip from blacklist or it may not exist"
fi
}

# Execute based on command
case "$COMMAND" in
"add")
add_rule "$SRCIP"
;;
"delete")
remove_rule "$SRCIP"
;;
*)
log_message "ERROR: Unknown command: $COMMAND"
exit 1
;;
esac

exit 0
9 changes: 9 additions & 0 deletions Wazuh/local_decoders.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- Sample Log -->

<!-- Aug 10 19:12:33 intern-phxdc-pve1 kernel: SSH_NEW: IN=vmbr0 OUT= MAC=24:6e:96:53:53:0a:90:b1:1c:06:d2:df:08:00 SRC=68.59.164.131 DST=10.15.0.4 LEN=64 TOS=0x00 PREC=0x00 TTL=52 ID=0 DF PROTO=TCP SPT=56148 DPT=2359 WINDOW=65535 RES=0x00 CWR ECE SYN URGP=0 -->
<decoder name="hypervisor-ssh-bruteforce">
<program_name>kernel</program_name>
<prematch>SSH_NEW: IN=</prematch>
<regex>SSH_NEW: IN=(\S+) OUT=(\S*) MAC=(\S+) SRC=(\S+) DST=(\S+) LEN=(\d+) TOS=(\S+) PREC=(\S+) TTL=(\d+) ID=(\d+) DF PROTO=(\S+) SPT=(\d+) DPT=(\d+) WINDOW=(\d+) RES=(\S+) CWR ECE SYN URGP=(\d+)</regex>
<order>in_interface,out_interface,mac,srcip,dstip,len,tos,prec,ttl,id,protocol,srcport,dstport,window,res,urgp</order>
</decoder>
38 changes: 38 additions & 0 deletions Wazuh/local_rules.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!-- Modify it at your will. -->
<!-- Copyright (C) 2015, Wazuh Inc. -->

<!-- Example -->
<group name="local,syslog,sshd,">

<!--
Dec 10 01:02:02 host sshd[1234]: Failed none for root from 1.1.1.1 port 1066 ssh2
-->
<rule id="100001" level="5">
<if_sid>5716</if_sid>
<srcip>1.1.1.1</srcip>
<description>sshd: authentication failed from IP 1.1.1.1.</description>
<group>authentication_failed,pci_dss_10.2.4,pci_dss_10.2.5,</group>
</rule>

</group>

<group name="local,iptables,sshd,bruteforce,">

<!-- Base rule for SSH connection attempts detected by iptables -->
<rule id="100002" level="5">
<decoded_as>hypervisor-ssh-bruteforce</decoded_as>
<description>SSH connection attempt detected by iptables from $(srcip) to port $(dstport)</description>
</rule>

<!-- Frequency rule for SSH brute force detection -->
<rule id="100003" level="10" frequency="5" timeframe="60">
<if_matched_sid>100002</if_matched_sid>
<location>intern-phxdc-pve1</location>
<same_source_ip />
<same_dstport />
<description>Possible SSH brute force detected - $(frequency) attempts in $(timeframe) seconds from $(srcip)</description>
<mitre>
<id>T1110</id>
</mitre>
</rule>
</group>
140 changes: 140 additions & 0 deletions Wazuh/manage-agents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Script to manage Wazuh Agents on the Wazuh Manager
// Last Modified on August 7th, 2025 by Maxwell Klema
// -------------------------------------------------

const axios = require('axios');
const env = require('dotenv').config({ path: '/var/lib/vz/snippets/Wazuh/.env', quiet: true});

const authConfig = {
method: 'post',
url: 'https://wazuh-server.opensource.mieweb.org/security/user/authenticate',
maxBodyLength: Infinity,
headers: {
'Authorization': `Basic ${Buffer.from(`${process.env.API_USERNAME}:${process.env.API_PASSWORD}`).toString('base64')}`,
}
};

async function getJWTToken() {
const response = await axios.request(authConfig);
if (response.status !== 200) {
return null;
}
return response.data.data.token;
}

async function getAgents() {

const JWT = await getJWTToken();
if (!JWT) {
console.log('fail');
return;
}

let config = {
method: 'get',
url: 'https://wazuh-server.opensource.mieweb.org/agents?',
maxBodyLength: Infinity,
headers: {
'content-type': 'application/json',
'Authorization': `Bearer ${JWT}`,
}
}

axios.request(config).then((response) => {
const agents = response.data.data.affected_items;
if (!agents || agents.length === 0) {
console.log('fail');
return;
}

agents.forEach(agent => {
console.log(agent.name);
});
});
}

async function addAgent(containerName, containerIP) {

const JWT = await getJWTToken();
if (!JWT) {
console.log('fail');
return;
}

// Add the Agent to the Manager
let agentConfig = {
method: 'post',
url: 'https://wazuh-server.opensource.mieweb.org/agents?pretty=true',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${JWT}`
},
data: {
'name': containerName,
'ip': containerIP
}
};

const response = await axios.request(agentConfig);
if (response.status !== 200) {
console.log('fail');
}
const agentKey = response.data.data.key;
console.log(agentKey);
}

async function deleteAgent(agentName) {

const JWT = await getJWTToken();

if (!JWT) {
console.log('fail');
return;
}

const agent_id = await getAgentIDByName(agentName, JWT);

if (!agent_id) {
console.log('fail');
return;
}

let config = {
method: 'delete',
url: `https://wazuh-server.opensource.mieweb.org/agents/?agents_list=${agent_id}&status=all&older_than=0s`,
maxBodyLength: Infinity,
headers: {
'content-type': 'application/json',
'Authorization': `Bearer ${JWT}`,
}
};

axios.request(config).then((response) => {
if (response.status !== 200) {
console.log('fail');
return;
}
console.log('success');
}).catch((error) => {
console.log('fail');
});
}

async function getAgentIDByName(agentName, JWT) {
let config = {
method: 'get',
url: 'https://wazuh-server.opensource.mieweb.org/agents/?name=' + agentName,
maxBodyLength: Infinity,
headers: {
'Authorization': `Bearer ${JWT}`
}
};

const response = await axios.request(config);
if (response.status !== 200) {
return null;
}
return response.data.data.affected_items[0].id;
}

module.exports = { getAgents, addAgent, deleteAgent };
Loading