refactor: add audit

This commit is contained in:
Mauricio Siu
2024-12-15 19:55:45 -06:00
parent 413536a336
commit c0acdc5df1
6 changed files with 490 additions and 1 deletions

View File

@@ -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 (
<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">
<PcCase 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">
{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">Status</h3>
<p className="text-sm text-muted-foreground mb-4">
Shows the server configuration status
</p>
<div className="grid gap-2.5">
<StatusRow
label="Docker Installed"
isEnabled={data?.docker?.enabled}
description={
data?.docker?.enabled
? `Installed: ${data?.docker?.version}`
: undefined
}
/>
<StatusRow
label="RClone Installed"
isEnabled={data?.rclone?.enabled}
description={
data?.rclone?.enabled
? `Installed: ${data?.rclone?.version}`
: undefined
}
/>
<StatusRow
label="Nixpacks Installed"
isEnabled={data?.nixpacks?.enabled}
description={
data?.nixpacks?.enabled
? `Installed: ${data?.nixpacks?.version}`
: undefined
}
/>
<StatusRow
label="Buildpacks Installed"
isEnabled={data?.buildpacks?.enabled}
description={
data?.buildpacks?.enabled
? `Installed: ${data?.buildpacks?.version}`
: undefined
}
/>
<StatusRow
label="Docker Swarm Initialized"
isEnabled={data?.isSwarmInstalled}
description={
data?.isSwarmInstalled
? "Initialized"
: "Not Initialized"
}
/>
<StatusRow
label="Dokploy Network Created"
isEnabled={data?.isDokployNetworkInstalled}
description={
data?.isDokployNetworkInstalled
? "Created"
: "Not Created"
}
/>
<StatusRow
label="Main Directory Created"
isEnabled={data?.isMainDirectoryInstalled}
description={
data?.isMainDirectoryInstalled
? "Created"
: "Not Created"
}
/>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</CardContent>
);
};

View File

@@ -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) => {
</AlertBlock>
<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="deployments">Deployments</TabsTrigger>
<TabsTrigger value="validate">Validate</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
</TabsList>
<TabsContent
@@ -336,6 +338,14 @@ export const SetupServer = ({ serverId }: Props) => {
<ValidateServer serverId={serverId} />
</div>
</TabsContent>
<TabsContent
value="security"
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">
<SecuritySetup serverId={serverId} />
</div>
</TabsContent>
<TabsContent
value="gpu-setup"
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"

View File

@@ -26,6 +26,7 @@ import {
getPublicIpWithFallback,
haveActiveServices,
removeDeploymentsByServerId,
serverSecurity,
serverSetup,
serverValidate,
updateServerById,
@@ -166,6 +167,33 @@ 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 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 }) => {

View File

@@ -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";

View File

@@ -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<any>((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,
});
});
};

View File

@@ -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<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
}
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,
});
});
};