mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #901 from Dokploy/feat/security
Feat: add remote server audit
This commit is contained in:
@@ -0,0 +1,233 @@
|
|||||||
|
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, LockKeyhole, RefreshCw } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { StatusRow } from "./gpu-support";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SecurityAudit = ({ 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 (
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
|
<div className="flex flex-row gap-2 justify-between w-full max-sm:flex-col">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<LockKeyhole className="size-5" />
|
||||||
|
<CardTitle className="text-xl">
|
||||||
|
Setup Security Sugestions
|
||||||
|
</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>Check the security sugestions</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
isLoading={isRefreshing}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
await refetch();
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 w-full">
|
||||||
|
{isError && (
|
||||||
|
<AlertBlock type="error" className="w-full">
|
||||||
|
{error.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<AlertBlock type="info" className="w-full">
|
||||||
|
Ubuntu/Debian OS support is currently supported (Experimental)
|
||||||
|
</AlertBlock>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center text-muted-foreground py-4">
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
<span>Checking Server configuration</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid w-full gap-4">
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-1">UFW</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
UFW (Uncomplicated Firewall) is a simple firewall that can
|
||||||
|
be used to block incoming and outgoing traffic from your
|
||||||
|
server.
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-2.5">
|
||||||
|
<StatusRow
|
||||||
|
label="UFW Installed"
|
||||||
|
isEnabled={data?.ufw?.installed}
|
||||||
|
description={
|
||||||
|
data?.ufw?.installed
|
||||||
|
? "Installed (Recommended)"
|
||||||
|
: "Not Installed (UFW should be installed for security)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Status"
|
||||||
|
isEnabled={data?.ufw?.active}
|
||||||
|
description={
|
||||||
|
data?.ufw?.active
|
||||||
|
? "Active (Recommended)"
|
||||||
|
: "Not Active (UFW should be enabled for security)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Default Incoming"
|
||||||
|
isEnabled={data?.ufw?.defaultIncoming === "deny"}
|
||||||
|
description={
|
||||||
|
data?.ufw?.defaultIncoming === "deny"
|
||||||
|
? "Default: Deny (Recommended)"
|
||||||
|
: `Default: ${data?.ufw?.defaultIncoming} (Should be set to 'deny' for security)`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-1">SSH</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
SSH (Secure Shell) is a protocol that allows you to securely
|
||||||
|
connect to a server and execute commands on it.
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-2.5">
|
||||||
|
<StatusRow
|
||||||
|
label="Enabled"
|
||||||
|
isEnabled={data?.ssh.enabled}
|
||||||
|
description={
|
||||||
|
data?.ssh.enabled
|
||||||
|
? "Enabled"
|
||||||
|
: "Not Enabled (SSH should be enabled)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Key Auth"
|
||||||
|
isEnabled={data?.ssh.keyAuth}
|
||||||
|
description={
|
||||||
|
data?.ssh.keyAuth
|
||||||
|
? "Enabled (Recommended)"
|
||||||
|
: "Not Enabled (Key Authentication should be enabled)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Password Auth"
|
||||||
|
isEnabled={data?.ssh.passwordAuth === "no"}
|
||||||
|
description={
|
||||||
|
data?.ssh.passwordAuth === "no"
|
||||||
|
? "Disabled (Recommended)"
|
||||||
|
: "Enabled (Password Authentication should be disabled)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Permit Root Login"
|
||||||
|
isEnabled={data?.ssh.permitRootLogin === "no"}
|
||||||
|
description={
|
||||||
|
data?.ssh.permitRootLogin === "no"
|
||||||
|
? "Disabled (Recommended)"
|
||||||
|
: `Enabled: ${data?.ssh.permitRootLogin} (Root Login should be disabled)`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Use PAM"
|
||||||
|
isEnabled={data?.ssh.usePam === "no"}
|
||||||
|
description={
|
||||||
|
data?.ssh.usePam === "no"
|
||||||
|
? "Disabled (Recommended for key-based auth)"
|
||||||
|
: "Enabled (Should be disabled when using key-based auth)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-1">Fail2Ban</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Fail2Ban (Fail2Ban) is a service that can be used to prevent
|
||||||
|
brute force attacks on your server.
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-2.5">
|
||||||
|
<StatusRow
|
||||||
|
label="Installed"
|
||||||
|
isEnabled={data?.fail2ban.installed}
|
||||||
|
description={
|
||||||
|
data?.fail2ban.installed
|
||||||
|
? "Installed (Recommended)"
|
||||||
|
: "Not Installed (Fail2Ban should be installed for protection against brute force attacks)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatusRow
|
||||||
|
label="Enabled"
|
||||||
|
isEnabled={data?.fail2ban.enabled}
|
||||||
|
description={
|
||||||
|
data?.fail2ban.enabled
|
||||||
|
? "Enabled (Recommended)"
|
||||||
|
: "Not Enabled (Fail2Ban service should be enabled)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Active"
|
||||||
|
isEnabled={data?.fail2ban.active}
|
||||||
|
description={
|
||||||
|
data?.fail2ban.active
|
||||||
|
? "Active (Recommended)"
|
||||||
|
: "Not Active (Fail2Ban service should be running)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatusRow
|
||||||
|
label="SSH Protection"
|
||||||
|
isEnabled={data?.fail2ban.sshEnabled === "true"}
|
||||||
|
description={
|
||||||
|
data?.fail2ban.sshEnabled === "true"
|
||||||
|
? "Enabled (Recommended)"
|
||||||
|
: "Not Enabled (SSH protection should be enabled to prevent brute force attacks)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatusRow
|
||||||
|
label="SSH Mode"
|
||||||
|
isEnabled={data?.fail2ban.sshMode === "aggressive"}
|
||||||
|
description={
|
||||||
|
data?.fail2ban.sshMode === "aggressive"
|
||||||
|
? "Aggressive Mode (Recommended)"
|
||||||
|
: `Mode: ${data?.fail2ban.sshMode || "Not Set"} (Aggressive mode recommended for better protection)`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -35,6 +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 { SecurityAudit } from "./security-audit";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -97,10 +98,11 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
|
|
||||||
<Tabs defaultValue="ssh-keys">
|
<Tabs defaultValue="ssh-keys">
|
||||||
<TabsList className="grid grid-cols-4 w-[600px]">
|
<TabsList className="grid grid-cols-5 w-[700px]">
|
||||||
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
<TabsTrigger value="validate">Validate</TabsTrigger>
|
<TabsTrigger value="validate">Validate</TabsTrigger>
|
||||||
|
<TabsTrigger value="audit">Security</TabsTrigger>
|
||||||
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
|
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
@@ -336,6 +338,14 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
<ValidateServer serverId={serverId} />
|
<ValidateServer serverId={serverId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent
|
||||||
|
value="audit"
|
||||||
|
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">
|
||||||
|
<SecurityAudit serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="gpu-setup"
|
value="gpu-setup"
|
||||||
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"
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const ShowServers = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
{query?.success && <WelcomeSuscription />}
|
{query?.success && isCloud && <WelcomeSuscription />}
|
||||||
<div className="space-y-2 flex flex-row justify-between items-end">
|
<div className="space-y-2 flex flex-row justify-between items-end">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const CreateSSHKey = () => {
|
|||||||
<Card className="h-full bg-transparent">
|
<Card className="h-full bg-transparent">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid w-full gap-4 pt-4">
|
<div className="grid w-full gap-4 pt-4">
|
||||||
{isLoading ? (
|
{isLoading || !cloudSSHKey ? (
|
||||||
<div className="min-h-[25vh] justify-center flex items-center gap-4">
|
<div className="min-h-[25vh] justify-center flex items-center gap-4">
|
||||||
<Loader2
|
<Loader2
|
||||||
className="animate-spin text-muted-foreground"
|
className="animate-spin text-muted-foreground"
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const Setup = () => {
|
|||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<Label>Select a server</Label>
|
<Label>Select the server and click on setup server</Label>
|
||||||
<Select onValueChange={setServerId} defaultValue={serverId}>
|
<Select onValueChange={setServerId} defaultValue={serverId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a server" />
|
<SelectValue placeholder="Select a server" />
|
||||||
|
|||||||
@@ -18,23 +18,11 @@ import { CreateServer } from "./create-server";
|
|||||||
import { CreateSSHKey } from "./create-ssh-key";
|
import { CreateSSHKey } from "./create-ssh-key";
|
||||||
import { Setup } from "./setup";
|
import { Setup } from "./setup";
|
||||||
import { Verify } from "./verify";
|
import { Verify } from "./verify";
|
||||||
import {
|
import { Database, Globe, GitMerge, Users, Code2, Plug } from "lucide-react";
|
||||||
Database,
|
|
||||||
Globe,
|
|
||||||
GitMerge,
|
|
||||||
Code,
|
|
||||||
Users,
|
|
||||||
Code2,
|
|
||||||
Plug,
|
|
||||||
} from "lucide-react";
|
|
||||||
import ConfettiExplosion from "react-confetti-explosion";
|
import ConfettiExplosion from "react-confetti-explosion";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const { useStepper, steps, Scoped } = defineStepper(
|
export const { useStepper, steps, Scoped } = defineStepper(
|
||||||
{
|
{
|
||||||
id: "requisites",
|
id: "requisites",
|
||||||
@@ -56,7 +44,7 @@ export const { useStepper, steps, Scoped } = defineStepper(
|
|||||||
{ id: "complete", title: "Complete", description: "Checkout complete" },
|
{ id: "complete", title: "Complete", description: "Checkout complete" },
|
||||||
);
|
);
|
||||||
|
|
||||||
export const WelcomeSuscription = ({ children }: Props) => {
|
export const WelcomeSuscription = () => {
|
||||||
const [showConfetti, setShowConfetti] = useState(false);
|
const [showConfetti, setShowConfetti] = useState(false);
|
||||||
const stepper = useStepper();
|
const stepper = useStepper();
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
@@ -92,9 +80,10 @@ export const WelcomeSuscription = ({ children }: Props) => {
|
|||||||
<DialogTitle className="text-2xl text-center">
|
<DialogTitle className="text-2xl text-center">
|
||||||
Welcome To Dokploy Cloud 🎉
|
Welcome To Dokploy Cloud 🎉
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-center">
|
<DialogDescription className="text-center max-w-xl mx-auto">
|
||||||
Thank you for choosing Dokploy Cloud, before you start you need to
|
Thank you for choosing Dokploy Cloud! 🚀 We're excited to have you
|
||||||
configure your remote server
|
onboard. Before you dive in, you'll need to configure your remote
|
||||||
|
server to unlock all the features we offer.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
@@ -120,9 +109,7 @@ export const WelcomeSuscription = ({ children }: Props) => {
|
|||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
role="tab"
|
||||||
variant={
|
variant={
|
||||||
index <= stepper.current.index
|
index <= stepper.current.index ? "secondary" : "ghost"
|
||||||
? "default"
|
|
||||||
: "secondary"
|
|
||||||
}
|
}
|
||||||
aria-current={
|
aria-current={
|
||||||
stepper.current.id === step.id ? "step" : undefined
|
stepper.current.id === step.id ? "step" : undefined
|
||||||
@@ -130,7 +117,7 @@ export const WelcomeSuscription = ({ children }: Props) => {
|
|||||||
aria-posinset={index + 1}
|
aria-posinset={index + 1}
|
||||||
aria-setsize={steps.length}
|
aria-setsize={steps.length}
|
||||||
aria-selected={stepper.current.id === step.id}
|
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)}
|
onClick={() => stepper.goTo(step.id)}
|
||||||
>
|
>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
getPublicIpWithFallback,
|
getPublicIpWithFallback,
|
||||||
haveActiveServices,
|
haveActiveServices,
|
||||||
removeDeploymentsByServerId,
|
removeDeploymentsByServerId,
|
||||||
|
serverAudit,
|
||||||
serverSetup,
|
serverSetup,
|
||||||
serverValidate,
|
serverValidate,
|
||||||
updateServerById,
|
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
|
remove: protectedProcedure
|
||||||
.input(apiRemoveServer)
|
.input(apiRemoveServer)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export * from "./setup/server-setup";
|
|||||||
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";
|
||||||
|
export * from "./setup/server-audit";
|
||||||
|
|
||||||
export * from "./utils/backups/index";
|
export * from "./utils/backups/index";
|
||||||
export * from "./utils/backups/mariadb";
|
export * from "./utils/backups/mariadb";
|
||||||
|
|||||||
114
packages/server/src/setup/server-audit.ts
Normal file
114
packages/server/src/setup/server-audit.ts
Normal file
@@ -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<any>((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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user