+
Fail2Ban
+
+ Fail2Ban (Fail2Ban) is a service that can be used to prevent
+ brute force attacks on your server.
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts
index 725fbbaa0..51212fc20 100644
--- a/apps/dokploy/server/api/routers/server.ts
+++ b/apps/dokploy/server/api/routers/server.ts
@@ -26,6 +26,7 @@ import {
getPublicIpWithFallback,
haveActiveServices,
removeDeploymentsByServerId,
+ serverAudit,
serverSecurity,
serverSetup,
serverValidate,
@@ -179,11 +180,36 @@ export const serverRouter = createTRPCRouter({
message: "You are not authorized to validate this server",
});
}
- const response = await serverSecurity(input.serverId);
- return {} as unknown as {
- docker: {
+ const response = await serverAudit(input.serverId);
+ console.log(response);
+ return response as unknown as {
+ ufw: {
+ installed: boolean;
+ active: boolean;
+ defaultIncoming: string;
+ };
+ ssh: {
enabled: boolean;
- version: string;
+ keyAuth: boolean;
+ permitRootLogin: string;
+ passwordAuth: string;
+ usePam: string;
+ };
+ nonRootUser: {
+ hasValidSudoUser: boolean;
+ };
+ unattendedUpgrades: {
+ installed: boolean;
+ active: boolean;
+ updateEnabled: number;
+ upgradeEnabled: number;
+ };
+ fail2ban: {
+ installed: boolean;
+ enabled: boolean;
+ active: boolean;
+ sshEnabled: string;
+ sshMode: string;
};
};
} catch (error) {
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index 8f2b8f76b..360289936 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -43,6 +43,7 @@ export * from "./setup/server-security";
export * from "./setup/setup";
export * from "./setup/traefik-setup";
export * from "./setup/server-validate";
+export * from "./setup/server-audit";
export * from "./utils/backups/index";
export * from "./utils/backups/mariadb";
diff --git a/packages/server/src/setup/server-audit.ts b/packages/server/src/setup/server-audit.ts
index c3da68872..3bb894d52 100644
--- a/packages/server/src/setup/server-audit.ts
+++ b/packages/server/src/setup/server-audit.ts
@@ -26,18 +26,18 @@ const validateSsh = () => `
`;
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 '^$')
+ 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
+ userShell=\$(getent passwd "\$user" | cut -d: -f7)
+ if [[ "\$userShell" != "/usr/sbin/nologin" && "\$userShell" != "/bin/false" ]]; then
validUserFound=true
break
fi
- done <<< "$privilegedUsers"
+ done <<< "\$privilegedUsers"
echo "{\\"hasValidSudoUser\\": $validUserFound}"
`;
diff --git a/packages/server/src/setup/server-security.ts b/packages/server/src/setup/server-security.ts
index 5e4bc499d..718efe9a3 100644
--- a/packages/server/src/setup/server-security.ts
+++ b/packages/server/src/setup/server-security.ts
@@ -89,6 +89,32 @@ export const serverSecurity = async (serverId: string) => {
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)
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 3/6] 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,
- });
- });
-};
From 6c586f9606d44e69ef1a3ab4184dbdaa12d22748 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Sun, 15 Dec 2024 21:18:23 -0600
Subject: [PATCH 4/6] refactor: add experimental advice
---
.../components/dashboard/settings/servers/security-audit.tsx | 3 +++
apps/dokploy/server/api/routers/server.ts | 1 -
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx b/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx
index 6d469708a..475f2b8ff 100644
--- a/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx
@@ -63,6 +63,9 @@ export const SecurityAudit = ({ serverId }: Props) => {
+
+ Ubuntu/Debian OS support is currently supported (Experimental)
+
{isLoading ? (
diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts
index f258f2361..4076d6e93 100644
--- a/apps/dokploy/server/api/routers/server.ts
+++ b/apps/dokploy/server/api/routers/server.ts
@@ -27,7 +27,6 @@ import {
haveActiveServices,
removeDeploymentsByServerId,
serverAudit,
- serverSecurity,
serverSetup,
serverValidate,
updateServerById,
From 341af1bd07556cee36049634ee2e375766d4b187 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Sun, 15 Dec 2024 21:33:10 -0600
Subject: [PATCH 5/6] feat: add loader to enhance ux/ui
---
.../settings/servers/show-servers.tsx | 2 +-
.../servers/welcome-stripe/create-ssh-key.tsx | 2 +-
.../settings/servers/welcome-stripe/setup.tsx | 2 +-
.../welcome-stripe/welcome-suscription.tsx | 29 +++++--------------
4 files changed, 11 insertions(+), 24 deletions(-)
diff --git a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx
index afd16b94c..a174cd9cb 100644
--- a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx
@@ -46,7 +46,7 @@ export const ShowServers = () => {
return (
- {query?.success &&
}
+ {query?.success && isCloud &&
}
diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx
index cd263f432..740f79607 100644
--- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx
@@ -50,7 +50,7 @@ export const CreateSSHKey = () => {
- {isLoading ? (
+ {isLoading || !cloudSSHKey ? (
{
-
+