From c0acdc5df1ffa55d5efcb44a1b083943765f0456 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 15 Dec 2024 19:55:45 -0600 Subject: [PATCH] refactor: add audit --- .../settings/servers/security-setup.tsx | 151 ++++++++++++++++++ .../settings/servers/setup-server.tsx | 12 +- apps/dokploy/server/api/routers/server.ts | 28 ++++ packages/server/src/index.ts | 1 + packages/server/src/setup/server-audit.ts | 149 +++++++++++++++++ packages/server/src/setup/server-security.ts | 150 +++++++++++++++++ 6 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 apps/dokploy/components/dashboard/settings/servers/security-setup.tsx create mode 100644 packages/server/src/setup/server-audit.ts create 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-setup.tsx new file mode 100644 index 00000000..5026892e --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/security-setup.tsx @@ -0,0 +1,151 @@ +import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { Loader2, PcCase, RefreshCw } from "lucide-react"; +import { useState } from "react"; +import { StatusRow } from "./gpu-support"; + +interface Props { + serverId: string; +} + +export const SecuritySetup = ({ serverId }: Props) => { + const [isRefreshing, setIsRefreshing] = useState(false); + const { data, refetch, error, isLoading, isError } = + api.server.security.useQuery( + { serverId }, + { + enabled: !!serverId, + }, + ); + const utils = api.useUtils(); + return ( + +
+ + +
+
+
+ + + Setup Security Sugestions + +
+ Check the security sugestions +
+ +
+
+ {isError && ( + + {error.message} + + )} +
+
+ + + {isLoading ? ( +
+ + Checking Server configuration +
+ ) : ( +
+
+

Status

+

+ Shows the server configuration status +

+
+ + + + + + + +
+
+
+ )} +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx index 33d9bb6c..0321f587 100644 --- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx @@ -35,6 +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"; interface Props { serverId: string; @@ -97,10 +98,11 @@ export const SetupServer = ({ serverId }: Props) => { - + SSH Keys Deployments Validate + Security GPU Setup { + +
+ +
+
{ + try { + const server = await findServerById(input.serverId); + if (server.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to validate this server", + }); + } + const response = await serverSecurity(input.serverId); + return {} as unknown as { + docker: { + enabled: boolean; + version: string; + }; + }; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error instanceof Error ? error?.message : `Error: ${error}`, + cause: error as Error, + }); + } + }), remove: protectedProcedure .input(apiRemoveServer) .mutation(async ({ input, ctx }) => { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 41f2b0fd..8f2b8f76 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -39,6 +39,7 @@ export * from "./setup/config-paths"; export * from "./setup/postgres-setup"; export * from "./setup/redis-setup"; export * from "./setup/server-setup"; +export * from "./setup/server-security"; export * from "./setup/setup"; export * from "./setup/traefik-setup"; export * from "./setup/server-validate"; diff --git a/packages/server/src/setup/server-audit.ts b/packages/server/src/setup/server-audit.ts new file mode 100644 index 00000000..c3da6887 --- /dev/null +++ b/packages/server/src/setup/server-audit.ts @@ -0,0 +1,149 @@ +import { Client } from "ssh2"; +import { findServerById } from "../services/server"; + +const validateUfw = () => ` + if command -v ufw >/dev/null 2>&1; then + isInstalled=true + isActive=$(sudo ufw status | grep -q "Status: active" && echo true || echo false) + defaultIncoming=$(sudo ufw status verbose | grep "Default:" | grep "incoming" | awk '{print $2}') + echo "{\\"installed\\": $isInstalled, \\"active\\": $isActive, \\"defaultIncoming\\": \\"$defaultIncoming\\"}" + else + echo "{\\"installed\\": false, \\"active\\": false, \\"defaultIncoming\\": \\"unknown\\"}" + fi +`; + +const validateSsh = () => ` + if systemctl is-active --quiet sshd; then + isEnabled=true + hasKeyAuth=$(find "$HOME/.ssh" -type f -name "authorized_keys" 2>/dev/null | grep -q . && echo true || echo false) + permitRootLogin=$(sudo sshd -T | grep -i "^PermitRootLogin" | awk '{print $2}') + passwordAuth=$(sudo sshd -T | grep -i "^PasswordAuthentication" | awk '{print $2}') + usePam=$(sudo sshd -T | grep -i "^UsePAM" | awk '{print $2}') + echo "{\\"enabled\\": $isEnabled, \\"keyAuth\\": $hasKeyAuth, \\"permitRootLogin\\": \\"$permitRootLogin\\", \\"passwordAuth\\": \\"$passwordAuth\\", \\"usePam\\": \\"$usePam\\"}" + else + echo "{\\"enabled\\": false, \\"keyAuth\\": false, \\"permitRootLogin\\": \\"unknown\\", \\"passwordAuth\\": \\"unknown\\", \\"usePam\\": \\"unknown\\"}" + 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 + isEnabled=$(systemctl is-enabled --quiet fail2ban.service && echo true || echo false) + isActive=$(systemctl is-active --quiet fail2ban.service && echo true || echo false) + + if [ -f "/etc/fail2ban/jail.local" ]; then + sshEnabled=$(grep -A10 "^\\[sshd\\]" /etc/fail2ban/jail.local | grep "enabled" | awk '{print $NF}' | tr -d '[:space:]') + sshMode=$(grep -A10 "^\\[sshd\\]" /etc/fail2ban/jail.local | grep "^mode[[:space:]]*=[[:space:]]*aggressive" >/dev/null && echo "aggressive" || echo "normal") + echo "{\\"installed\\": $isInstalled, \\"enabled\\": $isEnabled, \\"active\\": $isActive, \\"sshEnabled\\": \\"$sshEnabled\\", \\"sshMode\\": \\"$sshMode\\"}" + else + echo "{\\"installed\\": $isInstalled, \\"enabled\\": $isEnabled, \\"active\\": $isActive, \\"sshEnabled\\": \\"false\\", \\"sshMode\\": \\"normal\\"}" + fi + else + echo "{\\"installed\\": false, \\"enabled\\": false, \\"active\\": false, \\"sshEnabled\\": \\"false\\", \\"sshMode\\": \\"normal\\"}" + fi +`; + +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"); + } + + 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}" + `; + + 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 new file mode 100644 index 00000000..5e4bc499 --- /dev/null +++ b/packages/server/src/setup/server-security.ts @@ -0,0 +1,150 @@ +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 + } + + + 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, + }); + }); +};