From dcb1ea37c36dd172654606117cf4591a01541763 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:16:14 -0600 Subject: [PATCH] feat: add server audit --- ...{security-setup.tsx => security-audit.tsx} | 28 +-- .../settings/servers/setup-server.tsx | 4 +- apps/dokploy/server/api/routers/server.ts | 1 - packages/server/src/index.ts | 1 - packages/server/src/setup/server-audit.ts | 147 ++++++--------- packages/server/src/setup/server-security.ts | 176 ------------------ 6 files changed, 73 insertions(+), 284 deletions(-) rename apps/dokploy/components/dashboard/settings/servers/{security-setup.tsx => security-audit.tsx} (92%) delete mode 100644 packages/server/src/setup/server-security.ts diff --git a/apps/dokploy/components/dashboard/settings/servers/security-setup.tsx b/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx similarity index 92% rename from apps/dokploy/components/dashboard/settings/servers/security-setup.tsx rename to apps/dokploy/components/dashboard/settings/servers/security-audit.tsx index c908a01ad..6d469708a 100644 --- a/apps/dokploy/components/dashboard/settings/servers/security-setup.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx @@ -8,7 +8,7 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { Loader2, LockKeyhole, PcCase, RefreshCw } from "lucide-react"; +import { Loader2, LockKeyhole, RefreshCw } from "lucide-react"; import { useState } from "react"; import { StatusRow } from "./gpu-support"; @@ -16,7 +16,7 @@ interface Props { serverId: string; } -export const SecuritySetup = ({ serverId }: Props) => { +export const SecurityAudit = ({ serverId }: Props) => { const [isRefreshing, setIsRefreshing] = useState(false); const { data, refetch, error, isLoading, isError } = api.server.security.useQuery( @@ -82,8 +82,8 @@ export const SecuritySetup = ({ serverId }: Props) => { label="UFW Installed" isEnabled={data?.ufw?.installed} description={ - data?.ufw?.installed - ? "Installed (Recommended)" + data?.ufw?.installed + ? "Installed (Recommended)" : "Not Installed (UFW should be installed for security)" } /> @@ -91,8 +91,8 @@ export const SecuritySetup = ({ serverId }: Props) => { label="Status" isEnabled={data?.ufw?.active} description={ - data?.ufw?.active - ? "Active (Recommended)" + data?.ufw?.active + ? "Active (Recommended)" : "Not Active (UFW should be enabled for security)" } /> @@ -119,7 +119,9 @@ export const SecuritySetup = ({ serverId }: Props) => { label="Enabled" isEnabled={data?.ssh.enabled} description={ - data?.ssh.enabled ? "Enabled" : "Not Enabled (SSH should be enabled)" + data?.ssh.enabled + ? "Enabled" + : "Not Enabled (SSH should be enabled)" } /> { label="Installed" isEnabled={data?.fail2ban.installed} description={ - data?.fail2ban.installed - ? "Installed (Recommended)" + data?.fail2ban.installed + ? "Installed (Recommended)" : "Not Installed (Fail2Ban should be installed for protection against brute force attacks)" } /> @@ -182,8 +184,8 @@ export const SecuritySetup = ({ serverId }: Props) => { label="Enabled" isEnabled={data?.fail2ban.enabled} description={ - data?.fail2ban.enabled - ? "Enabled (Recommended)" + data?.fail2ban.enabled + ? "Enabled (Recommended)" : "Not Enabled (Fail2Ban service should be enabled)" } /> @@ -191,8 +193,8 @@ export const SecuritySetup = ({ serverId }: Props) => { label="Active" isEnabled={data?.fail2ban.active} description={ - data?.fail2ban.active - ? "Active (Recommended)" + data?.fail2ban.active + ? "Active (Recommended)" : "Not Active (Fail2Ban service should be running)" } /> diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx index 0321f587f..ff5c51c6e 100644 --- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx @@ -35,7 +35,7 @@ import { ShowDeployment } from "../../application/deployments/show-deployment"; import { EditScript } from "./edit-script"; import { GPUSupport } from "./gpu-support"; import { ValidateServer } from "./validate-server"; -import { SecuritySetup } from "./security-setup"; +import { SecurityAudit } from "./security-audit"; interface Props { serverId: string; @@ -343,7 +343,7 @@ export const SetupServer = ({ serverId }: Props) => { className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0" >
- +
` if command -v ufw >/dev/null 2>&1; then isInstalled=true @@ -25,40 +26,6 @@ const validateSsh = () => ` fi `; -const validateNonRootUser = () => ` - sudoUsers=\$(grep -Po '^sudo:.*:\\K.*$' /etc/group | tr ',' '\\n' | grep -v root) - adminUsers=\$(grep -Po '^admin:.*:\\K.*$' /etc/group | tr ',' '\\n' | grep -v root) - privilegedUsers=\$(echo -e "\${sudoUsers}\\n\${adminUsers}" | sort -u | grep -v '^$') - validUserFound=false - - while IFS= read -r user; do - userShell=\$(getent passwd "\$user" | cut -d: -f7) - if [[ "\$userShell" != "/usr/sbin/nologin" && "\$userShell" != "/bin/false" ]]; then - validUserFound=true - break - fi - done <<< "\$privilegedUsers" - - echo "{\\"hasValidSudoUser\\": $validUserFound}" -`; - -const validateUnattendedUpgrades = () => ` - if dpkg -l | grep -q "unattended-upgrades"; then - isInstalled=true - isActive=$(systemctl is-active --quiet unattended-upgrades.service && echo true || echo false) - - if [ -f "/etc/apt/apt.conf.d/20auto-upgrades" ]; then - updateEnabled=$(grep "APT::Periodic::Update-Package-Lists" "/etc/apt/apt.conf.d/20auto-upgrades" | grep -o '[0-9]\\+' || echo "0") - upgradeEnabled=$(grep "APT::Periodic::Unattended-Upgrade" "/etc/apt/apt.conf.d/20auto-upgrades" | grep -o '[0-9]\\+' || echo "0") - echo "{\\"installed\\": $isInstalled, \\"active\\": $isActive, \\"updateEnabled\\": $updateEnabled, \\"upgradeEnabled\\": $upgradeEnabled}" - else - echo "{\\"installed\\": $isInstalled, \\"active\\": $isActive, \\"updateEnabled\\": 0, \\"upgradeEnabled\\": 0}" - fi - else - echo "{\\"installed\\": false, \\"active\\": false, \\"updateEnabled\\": 0, \\"upgradeEnabled\\": 0}" - fi -`; - const validateFail2ban = () => ` if dpkg -l | grep -q "fail2ban"; then isInstalled=true @@ -78,72 +45,70 @@ const validateFail2ban = () => ` `; export const serverAudit = async (serverId: string) => { - const client = new Client(); - const server = await findServerById(serverId); - if (!server.sshKeyId) { - throw new Error("No SSH Key found"); - } + const client = new Client(); + const server = await findServerById(serverId); + if (!server.sshKeyId) { + throw new Error("No SSH Key found"); + } - return new Promise((resolve, reject) => { - client - .once("ready", () => { - const bashCommand = ` + return new Promise((resolve, reject) => { + client + .once("ready", () => { + const bashCommand = ` command_exists() { command -v "$@" > /dev/null 2>&1 } ufwStatus=$(${validateUfw()}) sshStatus=$(${validateSsh()}) - nonRootStatus=$(${validateNonRootUser()}) - upgradesStatus=$(${validateUnattendedUpgrades()}) fail2banStatus=$(${validateFail2ban()}) - echo "{\\"ufw\\": $ufwStatus, \\"ssh\\": $sshStatus, \\"nonRootUser\\": $nonRootStatus, \\"unattendedUpgrades\\": $upgradesStatus, \\"fail2ban\\": $fail2banStatus}" + echo "{\\"ufw\\": $ufwStatus, \\"ssh\\": $sshStatus, \\"fail2ban\\": $fail2banStatus}" `; - client.exec(bashCommand, (err, stream) => { - if (err) { - reject(err); - return; - } - let output = ""; - stream - .on("close", () => { - client.end(); - try { - const result = JSON.parse(output.trim()); - resolve(result); - } catch (parseError) { - reject( - new Error( - `Failed to parse output: ${parseError instanceof Error ? parseError.message : parseError}`, - ), - ); - } - }) - .on("data", (data: string) => { - output += data; - }) - .stderr.on("data", (data) => {}); - }); - }) - .on("error", (err) => { - client.end(); - if (err.level === "client-authentication") { - reject( - new Error( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ), - ); - } else { - reject(new Error(`SSH connection error: ${err.message}`)); - } - }) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: server.sshKey?.privateKey, - }); - }); + client.exec(bashCommand, (err, stream) => { + if (err) { + reject(err); + return; + } + let output = ""; + stream + .on("close", () => { + client.end(); + try { + const result = JSON.parse(output.trim()); + resolve(result); + } catch (parseError) { + reject( + new Error( + `Failed to parse output: ${parseError instanceof Error ? parseError.message : parseError}`, + ), + ); + } + }) + .on("data", (data: string) => { + output += data; + }) + .stderr.on("data", (data) => {}); + }); + }) + .on("error", (err) => { + client.end(); + if (err.level === "client-authentication") { + reject( + new Error( + `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, + ), + ); + } else { + reject(new Error(`SSH connection error: ${err.message}`)); + } + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: server.sshKey?.privateKey, + }); + }); }; diff --git a/packages/server/src/setup/server-security.ts b/packages/server/src/setup/server-security.ts deleted file mode 100644 index 718efe9a3..000000000 --- a/packages/server/src/setup/server-security.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { Client } from "ssh2"; -import { findServerById } from "../services/server"; - -const validateDocker = () => ` - if command_exists docker; then - echo "$(docker --version | awk '{print $3}' | sed 's/,//') true" - else - echo "0.0.0 false" - fi -`; - -const validateRClone = () => ` - if command_exists rclone; then - echo "$(rclone --version | head -n 1 | awk '{print $2}' | sed 's/^v//') true" - else - echo "0.0.0 false" - fi -`; - -const validateSwarm = () => ` - if docker info --format '{{.Swarm.LocalNodeState}}' | grep -q 'active'; then - echo true - else - echo false - fi -`; - -const validateNixpacks = () => ` - if command_exists nixpacks; then - version=$(nixpacks --version | awk '{print $2}') - if [ -n "$version" ]; then - echo "$version true" - else - echo "0.0.0 false" - fi - else - echo "0.0.0 false" - fi -`; - -const validateBuildpacks = () => ` - if command_exists pack; then - version=$(pack --version | awk '{print $1}') - if [ -n "$version" ]; then - echo "$version true" - else - echo "0.0.0 false" - fi - else - echo "0.0.0 false" - fi -`; - -const validateMainDirectory = () => ` - if [ -d "/etc/dokploy" ]; then - echo true - else - echo false - fi -`; - -const validateDokployNetwork = () => ` - if docker network ls | grep -q 'dokploy-network'; then - echo true - else - echo false - fi -`; - -export const serverSecurity = async (serverId: string) => { - const client = new Client(); - const server = await findServerById(serverId); - if (!server.sshKeyId) { - throw new Error("No SSH Key found"); - } - - return new Promise((resolve, reject) => { - client - .once("ready", () => { - const bashCommand = ` - set -u; - check_os() { - if [ -f /etc/lsb-release ]; then - echo "ubuntu" - elif [ -f /etc/debian_version ]; then - echo "debian" - else - echo "" - fi - } - - check_dependencies() { - echo -e "Checking required dependencies..." - - local required_commands=("curl" "jq" "systemctl" "apt-get") - local missing_commands=() - - for cmd in "\${required_commands[@]}"; do - if ! command -v "\$cmd" >/dev/null 2>&1; then - missing_commands+=("\$cmd") - fi - done - - if [ \${#missing_commands[@]} -ne 0 ]; then - echo -e "\${RED}The following required commands are missing:\${NC}" - for cmd in "\${missing_commands[@]}"; do - echo " - \$cmd" - done - echo - echo -e "\${YELLOW}Please install these commands before running this script.\${NC}" - exit 1 - fi - - echo -e "All required dependencies are installed\n" - return 0 - } - - - os=$(check_os) - - if [ -z "$os" ]; then - echo "This script only supports Ubuntu/Debian systems. Exiting." - echo "Please ensure you're running this script on a supported operating system." - exit 1 - fi - - echo "Detected supported OS: $os" - echo "Installing requirements for OS: $os" - `; - client.exec(bashCommand, (err, stream) => { - if (err) { - reject(err); - return; - } - let output = ""; - stream - .on("close", () => { - client.end(); - try { - // const result = JSON.parse(output.trim()); - console.log("Output:", output); - resolve(output.trim()); - } catch (parseError) { - reject( - new Error( - `Failed to parse output: ${parseError instanceof Error ? parseError.message : parseError}`, - ), - ); - } - }) - .on("data", (data: string) => { - output += data; - }) - .stderr.on("data", (data) => {}); - }); - }) - .on("error", (err) => { - client.end(); - if (err.level === "client-authentication") { - reject( - new Error( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ), - ); - } else { - reject(new Error(`SSH connection error: ${err.message}`)); - } - }) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: server.sshKey?.privateKey, - }); - }); -};