mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add server audit
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
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 { useState } from "react";
|
||||||
import { StatusRow } from "./gpu-support";
|
import { StatusRow } from "./gpu-support";
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ interface Props {
|
|||||||
serverId: string;
|
serverId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SecuritySetup = ({ serverId }: Props) => {
|
export const SecurityAudit = ({ serverId }: Props) => {
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const { data, refetch, error, isLoading, isError } =
|
const { data, refetch, error, isLoading, isError } =
|
||||||
api.server.security.useQuery(
|
api.server.security.useQuery(
|
||||||
@@ -82,8 +82,8 @@ export const SecuritySetup = ({ serverId }: Props) => {
|
|||||||
label="UFW Installed"
|
label="UFW Installed"
|
||||||
isEnabled={data?.ufw?.installed}
|
isEnabled={data?.ufw?.installed}
|
||||||
description={
|
description={
|
||||||
data?.ufw?.installed
|
data?.ufw?.installed
|
||||||
? "Installed (Recommended)"
|
? "Installed (Recommended)"
|
||||||
: "Not Installed (UFW should be installed for security)"
|
: "Not Installed (UFW should be installed for security)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -91,8 +91,8 @@ export const SecuritySetup = ({ serverId }: Props) => {
|
|||||||
label="Status"
|
label="Status"
|
||||||
isEnabled={data?.ufw?.active}
|
isEnabled={data?.ufw?.active}
|
||||||
description={
|
description={
|
||||||
data?.ufw?.active
|
data?.ufw?.active
|
||||||
? "Active (Recommended)"
|
? "Active (Recommended)"
|
||||||
: "Not Active (UFW should be enabled for security)"
|
: "Not Active (UFW should be enabled for security)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -119,7 +119,9 @@ export const SecuritySetup = ({ serverId }: Props) => {
|
|||||||
label="Enabled"
|
label="Enabled"
|
||||||
isEnabled={data?.ssh.enabled}
|
isEnabled={data?.ssh.enabled}
|
||||||
description={
|
description={
|
||||||
data?.ssh.enabled ? "Enabled" : "Not Enabled (SSH should be enabled)"
|
data?.ssh.enabled
|
||||||
|
? "Enabled"
|
||||||
|
: "Not Enabled (SSH should be enabled)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StatusRow
|
<StatusRow
|
||||||
@@ -172,8 +174,8 @@ export const SecuritySetup = ({ serverId }: Props) => {
|
|||||||
label="Installed"
|
label="Installed"
|
||||||
isEnabled={data?.fail2ban.installed}
|
isEnabled={data?.fail2ban.installed}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.installed
|
data?.fail2ban.installed
|
||||||
? "Installed (Recommended)"
|
? "Installed (Recommended)"
|
||||||
: "Not Installed (Fail2Ban should be installed for protection against brute force attacks)"
|
: "Not Installed (Fail2Ban should be installed for protection against brute force attacks)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -182,8 +184,8 @@ export const SecuritySetup = ({ serverId }: Props) => {
|
|||||||
label="Enabled"
|
label="Enabled"
|
||||||
isEnabled={data?.fail2ban.enabled}
|
isEnabled={data?.fail2ban.enabled}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.enabled
|
data?.fail2ban.enabled
|
||||||
? "Enabled (Recommended)"
|
? "Enabled (Recommended)"
|
||||||
: "Not Enabled (Fail2Ban service should be enabled)"
|
: "Not Enabled (Fail2Ban service should be enabled)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -191,8 +193,8 @@ export const SecuritySetup = ({ serverId }: Props) => {
|
|||||||
label="Active"
|
label="Active"
|
||||||
isEnabled={data?.fail2ban.active}
|
isEnabled={data?.fail2ban.active}
|
||||||
description={
|
description={
|
||||||
data?.fail2ban.active
|
data?.fail2ban.active
|
||||||
? "Active (Recommended)"
|
? "Active (Recommended)"
|
||||||
: "Not Active (Fail2Ban service should be running)"
|
: "Not Active (Fail2Ban service should be running)"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -35,7 +35,7 @@ import { ShowDeployment } from "../../application/deployments/show-deployment";
|
|||||||
import { EditScript } from "./edit-script";
|
import { EditScript } from "./edit-script";
|
||||||
import { GPUSupport } from "./gpu-support";
|
import { GPUSupport } from "./gpu-support";
|
||||||
import { ValidateServer } from "./validate-server";
|
import { ValidateServer } from "./validate-server";
|
||||||
import { SecuritySetup } from "./security-setup";
|
import { SecurityAudit } from "./security-audit";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId: string;
|
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"
|
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
|
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
|
||||||
<SecuritySetup serverId={serverId} />
|
<SecurityAudit serverId={serverId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
|
|||||||
@@ -181,7 +181,6 @@ export const serverRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const response = await serverAudit(input.serverId);
|
const response = await serverAudit(input.serverId);
|
||||||
console.log(response);
|
|
||||||
return response as unknown as {
|
return response as unknown as {
|
||||||
ufw: {
|
ufw: {
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ export * from "./setup/config-paths";
|
|||||||
export * from "./setup/postgres-setup";
|
export * from "./setup/postgres-setup";
|
||||||
export * from "./setup/redis-setup";
|
export * from "./setup/redis-setup";
|
||||||
export * from "./setup/server-setup";
|
export * from "./setup/server-setup";
|
||||||
export * from "./setup/server-security";
|
|
||||||
export * from "./setup/setup";
|
export * from "./setup/setup";
|
||||||
export * from "./setup/traefik-setup";
|
export * from "./setup/traefik-setup";
|
||||||
export * from "./setup/server-validate";
|
export * from "./setup/server-validate";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Client } from "ssh2";
|
import { Client } from "ssh2";
|
||||||
import { findServerById } from "../services/server";
|
import { findServerById } from "../services/server";
|
||||||
|
|
||||||
|
// Thanks for the idea to https://github.com/healthyhost/audit-vps-script/tree/main
|
||||||
const validateUfw = () => `
|
const validateUfw = () => `
|
||||||
if command -v ufw >/dev/null 2>&1; then
|
if command -v ufw >/dev/null 2>&1; then
|
||||||
isInstalled=true
|
isInstalled=true
|
||||||
@@ -25,40 +26,6 @@ const validateSsh = () => `
|
|||||||
fi
|
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 = () => `
|
const validateFail2ban = () => `
|
||||||
if dpkg -l | grep -q "fail2ban"; then
|
if dpkg -l | grep -q "fail2ban"; then
|
||||||
isInstalled=true
|
isInstalled=true
|
||||||
@@ -78,72 +45,70 @@ const validateFail2ban = () => `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const serverAudit = async (serverId: string) => {
|
export const serverAudit = async (serverId: string) => {
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
const server = await findServerById(serverId);
|
const server = await findServerById(serverId);
|
||||||
if (!server.sshKeyId) {
|
if (!server.sshKeyId) {
|
||||||
throw new Error("No SSH Key found");
|
throw new Error("No SSH Key found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<any>((resolve, reject) => {
|
return new Promise<any>((resolve, reject) => {
|
||||||
client
|
client
|
||||||
.once("ready", () => {
|
.once("ready", () => {
|
||||||
const bashCommand = `
|
const bashCommand = `
|
||||||
command_exists() {
|
command_exists() {
|
||||||
command -v "$@" > /dev/null 2>&1
|
command -v "$@" > /dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
ufwStatus=$(${validateUfw()})
|
ufwStatus=$(${validateUfw()})
|
||||||
sshStatus=$(${validateSsh()})
|
sshStatus=$(${validateSsh()})
|
||||||
nonRootStatus=$(${validateNonRootUser()})
|
|
||||||
upgradesStatus=$(${validateUnattendedUpgrades()})
|
|
||||||
fail2banStatus=$(${validateFail2ban()})
|
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) => {
|
client.exec(bashCommand, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let output = "";
|
let output = "";
|
||||||
stream
|
stream
|
||||||
.on("close", () => {
|
.on("close", () => {
|
||||||
client.end();
|
client.end();
|
||||||
try {
|
try {
|
||||||
const result = JSON.parse(output.trim());
|
const result = JSON.parse(output.trim());
|
||||||
resolve(result);
|
resolve(result);
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
`Failed to parse output: ${parseError instanceof Error ? parseError.message : parseError}`,
|
`Failed to parse output: ${parseError instanceof Error ? parseError.message : parseError}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("data", (data: string) => {
|
.on("data", (data: string) => {
|
||||||
output += data;
|
output += data;
|
||||||
})
|
})
|
||||||
.stderr.on("data", (data) => {});
|
.stderr.on("data", (data) => {});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.on("error", (err) => {
|
.on("error", (err) => {
|
||||||
client.end();
|
client.end();
|
||||||
if (err.level === "client-authentication") {
|
if (err.level === "client-authentication") {
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`SSH connection error: ${err.message}`));
|
reject(new Error(`SSH connection error: ${err.message}`));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.connect({
|
.connect({
|
||||||
host: server.ipAddress,
|
host: server.ipAddress,
|
||||||
port: server.port,
|
port: server.port,
|
||||||
username: server.username,
|
username: server.username,
|
||||||
privateKey: server.sshKey?.privateKey,
|
privateKey: server.sshKey?.privateKey,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<string>((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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user