server_secure
Harden Kastell server security by applying SSH hardening, configuring firewalls, managing domains, and running security audits through specific actions.
Instructions
Secure Kastell servers. Secure: 'secure-setup' applies SSH hardening + fail2ban, 'secure-audit' runs security audit with score. Firewall: 'firewall-setup' installs UFW with Coolify ports, 'firewall-add'/'firewall-remove' manage port rules, 'firewall-status' shows current rules. Domain: 'domain-set'/'domain-remove' manage custom domain with optional SSL, 'domain-check' verifies DNS, 'domain-info' shows current FQDN. All require SSH access to server. For full one-shot hardening (SSH + fail2ban + UFW + sysctl + unattended-upgrades), use server_lock instead.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | Action: Secure: 'secure-setup' hardens SSH + installs fail2ban, 'secure-audit' runs security audit with score. Firewall: 'firewall-setup' installs UFW, 'firewall-add'/'firewall-remove' manage port rules, 'firewall-status' shows rules. Domain: 'domain-set'/'domain-remove' manage FQDN, 'domain-check' verifies DNS, 'domain-info' shows current FQDN. | |
| server | No | Server name or IP. Auto-selected if only one server exists. | |
| port | No | Port number. Required for firewall-add/remove. Optional SSH port for secure-setup. | |
| protocol | No | Protocol for firewall rules. Default: tcp. | tcp |
| domain | No | Domain name. Required for domain-set and domain-check. | |
| ssl | No | Enable SSL (https) for domain. Default: true. |
Implementation Reference
- src/mcp/tools/serverSecure.ts:52-110 (handler)Main handler for 'server_secure' tool that dispatches actions to specific handlers.
export async function handleServerSecure(params: { action: Action; server?: string; port?: number; protocol?: "tcp" | "udp"; domain?: string; ssl?: boolean; }, mcpServer?: McpServer): Promise<McpResponse> { try { const servers = getServers(); if (servers.length === 0) { return mcpError("No servers found", undefined, [ { command: "kastell init", reason: "Deploy a server first" }, ]); } const server = resolveServerForMcp(params, servers); if (!server) { if (params.server) { return mcpError( `Server not found: ${params.server}`, `Available servers: ${servers.map((s) => s.name).join(", ")}`, ); } return mcpError( "Multiple servers found. Specify which server to use.", `Available: ${servers.map((s) => s.name).join(", ")}`, ); } const domainActions = ["domain-set", "domain-remove", "domain-check", "domain-info"]; if (domainActions.includes(params.action)) { const modeError = requireManagedMode(server, params.action); if (modeError) { return mcpError(modeError, "Domain management requires a managed platform (Coolify or Dokploy). Use SSH for bare server DNS configuration."); } } await mcpLog(mcpServer, `Applying ${params.action} on ${server.name}`); switch (params.action) { case "secure-setup": return handleSecureSetup(server, params.port); case "secure-audit": return handleSecureAudit(server); case "firewall-setup": return handleFirewallSetup(server); case "firewall-add": return handleFirewallAdd(server, params.port, params.protocol || "tcp"); case "firewall-remove": return handleFirewallRemove(server, params.port, params.protocol || "tcp"); case "firewall-status": return handleFirewallStatus(server); case "domain-set": return handleDomainSet(server, params.domain, params.ssl ?? true); case "domain-remove": return handleDomainRemove(server); case "domain-check": return handleDomainCheck(server, params.domain); case "domain-info": return handleDomainInfo(server); default: { return mcpError(`Unknown action: ${params.action as string}`); } } } catch (error: unknown) { return mcpError(getErrorMessage(error)); } } - Specific implementation logic for security, firewall, and domain actions used by 'server_secure'.
export async function handleSecureSetup( server: ServerRecord, port: number | undefined, ): Promise<McpResponse> { const result = await applySecureSetup(server.ip, port ? { port } : undefined); if (!result.success) { return { content: [{ type: "text", text: JSON.stringify({ server: server.name, ip: server.ip, error: result.error, ...(result.hint ? { hint: result.hint } : {}), suggested_actions: [ { command: `server_info { action: 'health', server: '${server.name}' }`, reason: "Check if server is reachable" }, ], }) }], isError: true, }; } const message = result.fail2ban ? "Security setup complete: SSH hardened + fail2ban active" : "Security setup partially complete: SSH hardened, fail2ban failed"; return { content: [{ type: "text", text: JSON.stringify({ success: true, server: server.name, ip: server.ip, message, sshHardening: result.sshHardening, fail2ban: result.fail2ban, sshKeyCount: result.sshKeyCount, ...(result.hint ? { hint: result.hint } : {}), suggested_actions: [ { command: `server_secure { action: 'secure-audit', server: '${server.name}' }`, reason: "Verify security configuration" }, ], }) }], ...(!result.fail2ban ? { isError: true } : {}), }; } export async function handleSecureAudit(server: ServerRecord): Promise<McpResponse> { const result = await runSecureAudit(server.ip); if (result.error) { return { content: [{ type: "text", text: JSON.stringify({ server: server.name, ip: server.ip, error: result.error, ...(result.hint ? { hint: result.hint } : {}), }) }], isError: true, }; } const suggestedActions = result.score < 100 ? [{ command: `server_secure { action: 'secure-setup', server: '${server.name}' }`, reason: "Improve security score" }] : [{ command: `server_secure { action: 'firewall-status', server: '${server.name}' }`, reason: "Check firewall configuration" }]; return mcpSuccess({ server: server.name, ip: server.ip, score: result.score, maxScore: 100, checks: { passwordAuth: result.audit.passwordAuth, rootLogin: result.audit.rootLogin, fail2ban: result.audit.fail2ban, sshPort: result.audit.sshPort, }, suggested_actions: suggestedActions, }); } // ─── Firewall handlers ──────────────────────────────────────────────────────── export async function handleFirewallSetup(server: ServerRecord): Promise<McpResponse> { const platform = resolvePlatform(server); const result = await setupFirewall(server.ip, platform); if (!result.success) { return { content: [{ type: "text", text: JSON.stringify({ server: server.name, ip: server.ip, error: result.error, ...(result.hint ? { hint: result.hint } : {}), }) }], isError: true, }; } const ports = getPortsForPlatform(platform); const platformLabel = platform ?? "bare"; return mcpSuccess({ success: true, server: server.name, ip: server.ip, message: `UFW enabled with ${platformLabel} ports (${ports.join(", ")}) + SSH (22)`, suggested_actions: [ { command: `server_secure { action: 'firewall-status', server: '${server.name}' }`, reason: "Verify firewall rules" }, ], }); } export async function handleFirewallAdd( server: ServerRecord, port: number | undefined, protocol: "tcp" | "udp", ): Promise<McpResponse> { if (port === undefined) { return mcpError( "Port is required for firewall-add action", "Specify a port number (1-65535)", ); } const result = await addFirewallRule(server.ip, port, protocol); if (!result.success) { return { content: [{ type: "text", text: JSON.stringify({ server: server.name, ip: server.ip, error: result.error, ...(result.hint ? { hint: result.hint } : {}), }) }], isError: true, }; } return mcpSuccess({ success: true, server: server.name, ip: server.ip, message: `Port ${port}/${protocol} opened`, suggested_actions: [ { command: `server_secure { action: 'firewall-status', server: '${server.name}' }`, reason: "Verify firewall rules" }, ], }); } export async function handleFirewallRemove( server: ServerRecord, port: number | undefined, protocol: "tcp" | "udp", ): Promise<McpResponse> { if (port === undefined) { return mcpError( "Port is required for firewall-remove action", "Specify a port number (1-65535)", ); } const platform = resolvePlatform(server); const result = await removeFirewallRule(server.ip, port, protocol, platform); if (!result.success) { return { content: [{ type: "text", text: JSON.stringify({ server: server.name, ip: server.ip, error: result.error, ...(result.hint ? { hint: result.hint } : {}), ...(result.warning ? { warning: result.warning } : {}), }) }], isError: true, }; } return mcpSuccess({ success: true, server: server.name, ip: server.ip, message: `Port ${port}/${protocol} closed`, ...(result.warning ? { warning: result.warning } : {}), suggested_actions: [ { command: `server_secure { action: 'firewall-status', server: '${server.name}' }`, reason: "Verify firewall rules" }, ], }); } export async function handleFirewallStatus(server: ServerRecord): Promise<McpResponse> { const result = await getFirewallStatus(server.ip); if (result.error) { return { content: [{ type: "text", text: JSON.stringify({ server: server.name, ip: server.ip, error: result.error, ...(result.hint ? { hint: result.hint } : {}), }) }], isError: true, }; } const suggestedActions = !result.status.active ? [{ command: `server_secure { action: 'firewall-setup', server: '${server.name}' }`, reason: "Enable firewall" }] : [{ command: `server_secure { action: 'firewall-add', server: '${server.name}', port: 3000 }`, reason: "Open additional ports if needed" }]; return mcpSuccess({ server: server.name, ip: server.ip, active: result.status.active, rules: result.status.rules, ruleCount: result.status.rules.length, suggested_actions: suggestedActions, }); } // ─── Domain handlers ────────────────────────────────────────────────────────── export async function handleDomainSet( server: ServerRecord, domainName: string | undefined, ssl: boolean, ): Promise<McpResponse> { if (!domainName) { return mcpError( "Domain is required for domain-set action", "Specify a domain name (e.g., coolify.example.com)", ); } const result = await setDomain(server.ip, domainName, ssl, resolvePlatform(server)); if (!result.success) { return { content: [{ type: "text", text: JSON.stringify({ server: server.name, ip: server.ip, error: result.error, ...(result.hint ? { hint: result.hint } : {}), }) }], isError: true, }; } const protocol = ssl ? "https" : "http"; return mcpSuccess({ success: true, server: server.name, ip: server.ip, message: `Domain set to ${domainName}`, url: `${protocol}://${domainName}`, suggested_actions: [ { command: `server_secure { action: 'domain-check', server: '${server.name}', domain: '${domainName}' }`, reason: "Verify DNS points to this server" }, { command: `server_info { action: 'health', server: '${server.name}' }`, reason: "Verify Coolify is accessible" }, ], }); } export async function handleDomainRemove(server: ServerRecord): Promise<McpResponse> { const platform = resolvePlatform(server); const result = await removeDomain(server.ip, platform); if (!result.success) { return { content: [{ type: "text", text: JSON.stringify({ server: server.name, ip: server.ip, error: result.error, ...(result.hint ? { hint: result.hint } : {}), }) }], isError: true, }; } return mcpSuccess({ success: true, server: server.name, ip: server.ip, message: "Domain removed. Platform reset to default.", url: `http://${server.ip}:${platform === "dokploy" ? DOKPLOY_PORT : COOLIFY_PORT}`, suggested_actions: [ { command: `server_info { action: 'health', server: '${server.name}' }`, reason: "Verify Coolify is accessible" }, ], }); } export async function handleDomainCheck( server: ServerRecord, domainName: string | undefined, ): Promise<McpResponse> { if (!domainName) { return mcpError( "Domain is required for domain-check action", "Specify a domain name to check DNS for", ); } const result = await checkDns(server.ip, domainName); if (result.error) { return { content: [{ type: "text", text: JSON.stringify({ server: server.name, ip: server.ip, domain: domainName, error: result.error, ...(result.hint ? { hint: result.hint } : {}), }) }], isError: true, }; } return mcpSuccess({ server: server.name, ip: server.ip, domain: domainName, resolvedIp: result.resolvedIp, match: result.match, ...(result.hint ? { hint: result.hint } : {}), suggested_actions: result.match ? [{ command: `server_secure { action: 'domain-set', server: '${server.name}', domain: '${domainName}' }`, reason: "Set this domain as Coolify FQDN" }] : [{ command: `server_secure { action: 'domain-info', server: '${server.name}' }`, reason: "Check current domain setting" }], }); } export async function handleDomainInfo(server: ServerRecord): Promise<McpResponse> { const platform = resolvePlatform(server); const result = await getDomain(server.ip, platform); if (result.error) { return { content: [{ type: "text", text: JSON.stringify({ server: server.name, ip: server.ip, error: result.error, ...(result.hint ? { hint: result.hint } : {}), }) }], isError: true, }; } const domainSuggestedActions = []; if (result.fqdn) { const cleanFqdn = result.fqdn.replace(/^https?:\/\//, ""); domainSuggestedActions.push({ command: `server_secure { action: 'domain-check', server: '${server.name}', domain: '${cleanFqdn}' }`, reason: "Verify DNS", }); } else { domainSuggestedActions.push({ command: `server_secure { action: 'domain-set', server: '${server.name}', domain: 'coolify.example.com' }`, reason: "Set a custom domain", }); } return mcpSuccess({ server: server.name, ip: server.ip, fqdn: result.fqdn, message: result.fqdn ? `Current domain: ${result.fqdn}` : `No custom domain set. Default: http://${server.ip}:${platform === "dokploy" ? DOKPLOY_PORT : COOLIFY_PORT}`, suggested_actions: domainSuggestedActions, }); } - src/mcp/tools/serverSecure.ts:25-48 (schema)Input schema definition for the 'server_secure' tool.
export const serverSecureSchema = { action: z.enum([ "secure-setup", "secure-audit", "firewall-setup", "firewall-add", "firewall-remove", "firewall-status", "domain-set", "domain-remove", "domain-check", "domain-info", ]).describe( "Action: Secure: 'secure-setup' hardens SSH + installs fail2ban, 'secure-audit' runs security audit with score. Firewall: 'firewall-setup' installs UFW, 'firewall-add'/'firewall-remove' manage port rules, 'firewall-status' shows rules. Domain: 'domain-set'/'domain-remove' manage FQDN, 'domain-check' verifies DNS, 'domain-info' shows current FQDN.", ), server: z.string().optional().describe( "Server name or IP. Auto-selected if only one server exists.", ), port: z.number().min(1).max(65535).optional().describe( "Port number. Required for firewall-add/remove. Optional SSH port for secure-setup.", ), protocol: z.enum(["tcp", "udp"]).default("tcp").describe( "Protocol for firewall rules. Default: tcp.", ), domain: z.string().optional().describe( "Domain name. Required for domain-set and domain-check.", ), ssl: z.boolean().default(true).describe( "Enable SSL (https) for domain. Default: true.", ), }; - src/mcp/server.ts:116-128 (registration)Tool registration for 'server_secure' in the MCP server.
server.registerTool("server_secure", { description: "Secure Kastell servers. Secure: 'secure-setup' applies SSH hardening + fail2ban, 'secure-audit' runs security audit with score. Firewall: 'firewall-setup' installs UFW with Coolify ports, 'firewall-add'/'firewall-remove' manage port rules, 'firewall-status' shows current rules. Domain: 'domain-set'/'domain-remove' manage custom domain with optional SSL, 'domain-check' verifies DNS, 'domain-info' shows current FQDN. All require SSH access to server. For full one-shot hardening (SSH + fail2ban + UFW + sysctl + unattended-upgrades), use server_lock instead.", inputSchema: serverSecureSchema, annotations: { title: "Server Security", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, }, async (params) => { return handleServerSecure(params, server);