@@ -120,9 +109,7 @@ export const WelcomeSuscription = ({ children }: Props) => {
type="button"
role="tab"
variant={
- index <= stepper.current.index
- ? "default"
- : "secondary"
+ index <= stepper.current.index ? "secondary" : "ghost"
}
aria-current={
stepper.current.id === step.id ? "step" : undefined
@@ -130,7 +117,7 @@ export const WelcomeSuscription = ({ children }: Props) => {
aria-posinset={index + 1}
aria-setsize={steps.length}
aria-selected={stepper.current.id === step.id}
- className="flex size-10 items-center justify-center rounded-full"
+ className="flex size-10 items-center justify-center rounded-full border-2 border-border"
onClick={() => stepper.goTo(step.id)}
>
{index + 1}
diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts
index c83e4938..4076d6e9 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,
serverSetup,
serverValidate,
updateServerById,
@@ -166,6 +167,57 @@ export const serverRouter = createTRPCRouter({
});
}
}),
+
+ security: protectedProcedure
+ .input(apiFindOneServer)
+ .query(async ({ input, ctx }) => {
+ 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 serverAudit(input.serverId);
+ return response as unknown as {
+ ufw: {
+ installed: boolean;
+ active: boolean;
+ defaultIncoming: string;
+ };
+ ssh: {
+ enabled: boolean;
+ 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) {
+ 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..d48e6ea8 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -42,6 +42,7 @@ export * from "./setup/server-setup";
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
new file mode 100644
index 00000000..df00e9a7
--- /dev/null
+++ b/packages/server/src/setup/server-audit.ts
@@ -0,0 +1,114 @@
+import { Client } from "ssh2";
+import { findServerById } from "../services/server";
+
+// Thanks for the idea to https://github.com/healthyhost/audit-vps-script/tree/main
+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 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()})
+ fail2banStatus=$(${validateFail2ban()})
+
+ 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,
+ });
+ });
+};