完成 SSH 登录监控

This commit is contained in:
dushixiang committed 2026-01-03 12:01:49 +08:00
1 parent 9d14a8cba5
commit 7386c6e8dd
6 files changed
+648 -476

No files matched your search

+1
View File
@@ -42,3 +42,4 @@ agent.yaml
playground
web/node_modules
web/dist
deploy.sh
+140 -3
View File
@@ -5,13 +5,16 @@ import (
"fmt"
"log/slog"
"os"
"os/exec"
"strings"
)
const (
PAMConfigFile = "/etc/pam.d/sshd"
PAMConfigLine = "session optional pam_exec.so /usr/local/bin/pika_ssh_hook.sh"
HookScriptPath = "/usr/local/bin/pika_ssh_hook.sh"
PAMConfigFile = "/etc/pam.d/sshd"
PAMConfigLine = "session optional pam_exec.so /usr/local/bin/pika_ssh_hook.sh"
HookScriptPath = "/usr/local/bin/pika_ssh_hook.sh"
SSHDConfigFile = "/etc/ssh/sshd_config"
UsePAMDirective = "UsePAM yes"
)
// HookManager PAM Hook 管理器
@@ -35,6 +38,11 @@ func (h *HookManager) Install() error {
return nil
}
// 确保 SSH 配置启用 PAM
if err := h.ensureUsePAM(); err != nil {
return fmt.Errorf("配置 SSH UsePAM 失败: %w", err)
}
// 部署 Hook 脚本
if err := h.deployScript(); err != nil {
return fmt.Errorf("部署脚本失败: %w", err)
@@ -218,3 +226,132 @@ func (h *HookManager) modifyPAMConfig(add bool) error {
slog.Info("PAM 配置修改成功", "add", add)
return nil
}
// ensureUsePAM 确保 SSH 配置启用 PAM
func (h *HookManager) ensureUsePAM() error {
// 读取现有配置
f, err := os.Open(SSHDConfigFile)
if err != nil {
return err
}
var lines []string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
f.Close()
if err := scanner.Err(); err != nil {
return err
}
// 检查和修改配置
var newLines []string
usePAMEnabled := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
// 检查是否是 UsePAM 配置行
if strings.HasPrefix(trimmed, "UsePAM") {
// 检查是否已启用
if strings.Contains(trimmed, "yes") && !strings.HasPrefix(trimmed, "#") {
usePAMEnabled = true
newLines = append(newLines, line)
} else {
// 注释掉旧的配置行
if !strings.HasPrefix(trimmed, "#") {
newLines = append(newLines, "# "+line)
} else {
newLines = append(newLines, line)
}
}
continue
}
newLines = append(newLines, line)
}
// 如果 UsePAM 已正确启用,无需修改
if usePAMEnabled {
slog.Info("SSH UsePAM 已启用,跳过")
return nil
}
// 如果找到了 UsePAM 但未启用,或者没找到,都需要添加配置
if !usePAMEnabled {
newLines = append(newLines, UsePAMDirective)
}
// 备份原配置
backupPath := SSHDConfigFile + ".bak"
if err := os.Rename(SSHDConfigFile, backupPath); err != nil {
return err
}
// 写入新配置
outF, err := os.Create(SSHDConfigFile)
if err != nil {
// 恢复备份
os.Rename(backupPath, SSHDConfigFile)
return err
}
defer outF.Close()
writer := bufio.NewWriter(outF)
for _, line := range newLines {
writer.WriteString(line + "\n")
}
if err := writer.Flush(); err != nil {
// 恢复备份
outF.Close()
os.Rename(backupPath, SSHDConfigFile)
return err
}
// 设置正确的权限
os.Chmod(SSHDConfigFile, 0600)
slog.Info("SSH UsePAM 配置成功")
// 重启 sshd 服务使配置生效
if err := h.restartSSHD(); err != nil {
slog.Warn("重启 sshd 服务失败,请手动重启", "error", err)
// 不返回错误,因为配置已经成功,只是需要手动重启
}
return nil
}
// restartSSHD 重启 sshd 服务
func (h *HookManager) restartSSHD() error {
// 尝试使用 systemctl (systemd)
cmd := exec.Command("systemctl", "restart", "sshd")
if err := cmd.Run(); err == nil {
slog.Info("已通过 systemctl 重启 sshd 服务")
return nil
}
// 尝试 service 命令 (SysV init)
cmd = exec.Command("service", "sshd", "restart")
if err := cmd.Run(); err == nil {
slog.Info("已通过 service 重启 sshd 服务")
return nil
}
// 尝试直接重启 (某些系统可能使用 ssh 而不是 sshd)
cmd = exec.Command("systemctl", "restart", "ssh")
if err := cmd.Run(); err == nil {
slog.Info("已通过 systemctl 重启 ssh 服务")
return nil
}
cmd = exec.Command("service", "ssh", "restart")
if err := cmd.Run(); err == nil {
slog.Info("已通过 service 重启 ssh 服务")
return nil
}
return fmt.Errorf("无法找到合适的方式重启 sshd 服务")
}
+3 -2
View File
@@ -16,11 +16,12 @@
"@tailwindcss/vite": "^4.1.14",
"@tanstack/react-query": "^5.90.6",
"@types/qs": "^6.14.0",
"antd": "^5.27.4",
"antd": "^6.1.3",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.18",
"lucide-react": "^0.545.0",
"qs": "^6.14.0",
"qs": "^6.14.1",
"rc-field-form": "^2.7.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.4",
+5 -4
View File
@@ -563,7 +563,7 @@ export interface GetSSHLoginEventsParams {
endTime?: number;
}
export const getSSHLoginEvents = (agentId: string, params?: GetSSHLoginEventsParams) => {
export const getSSHLoginEvents = async (agentId: string, params?: GetSSHLoginEventsParams) => {
const query = new URLSearchParams();
if (params?.page) query.append('page', params.page.toString());
if (params?.pageSize) query.append('pageSize', params.pageSize.toString());
@@ -573,10 +573,11 @@ export const getSSHLoginEvents = (agentId: string, params?: GetSSHLoginEventsPar
if (params?.startTime) query.append('startTime', params.startTime.toString());
if (params?.endTime) query.append('endTime', params.endTime.toString());
return get<SSHLoginEventListResponse>(`/admin/agents/${agentId}/ssh-login/events?${query.toString()}`);
const response = await get<SSHLoginEventListResponse>(`/admin/agents/${agentId}/ssh-login/events?${query.toString()}`);
return response.data;
};
// 删除 SSH 登录事件
export const deleteSSHLoginEvents = (agentId: string) => {
return del(`/admin/agents/${agentId}/ssh-login/events`);
export const deleteSSHLoginEvents = async (agentId: string) => {
await del(`/admin/agents/${agentId}/ssh-login/events`);
};
+11 -11
View File
@@ -1,6 +1,6 @@
import React, {useRef} from 'react';
import {App, Tag, Tooltip} from 'antd';
import {CheckCircle, Key, Terminal, XCircle} from 'lucide-react';
import {App, Button, Tag, Tooltip} from 'antd';
import {Terminal} from 'lucide-react';
import type {ActionType, ProColumns} from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import type {SSHLoginEvent} from '@/types';
@@ -13,7 +13,7 @@ interface SSHLoginEventsProps {
const SSHLoginEvents: React.FC<SSHLoginEventsProps> = ({agentId}) => {
const {message, modal} = App.useApp();
const actionRef = useRef<ActionType>();
const actionRef = useRef<ActionType>(null);
// 定义表格列
const columns: ProColumns<SSHLoginEvent>[] = [
@@ -42,9 +42,9 @@ const SSHLoginEvents: React.FC<SSHLoginEventsProps> = ({agentId}) => {
},
render: (_, record) => (
record.status === 'success' ? (
<Tag icon={<CheckCircle size={14}/>} color="success">成功</Tag>
<Tag variant={'filled'} color="success">成功</Tag>
) : (
<Tag icon={<XCircle size={14}/>} color="error">失败</Tag>
<Tag variant={'filled'} color="error">失败</Tag>
)
),
},
@@ -84,7 +84,7 @@ const SSHLoginEvents: React.FC<SSHLoginEventsProps> = ({agentId}) => {
record.method ? (
<Tooltip
title={record.method === 'password' ? '密码认证' : record.method === 'publickey' ? '公钥认证' : record.method}>
<Tag icon={<Key size={14}/>}>{record.method}</Tag>
<Tag>{record.method}</Tag>
</Tooltip>
) : '-'
),
@@ -122,8 +122,8 @@ const SSHLoginEvents: React.FC<SSHLoginEventsProps> = ({agentId}) => {
cancelText: '取消',
onOk: async () => {
try {
const response = await deleteSSHLoginEvents(agentId);
message.success(response.message || '所有事件已删除');
await deleteSSHLoginEvents(agentId);
message.success('所有事件已删除');
actionRef.current?.reload();
} catch (error: any) {
console.error('Failed to delete SSH login events:', error);
@@ -174,12 +174,12 @@ const SSHLoginEvents: React.FC<SSHLoginEventsProps> = ({agentId}) => {
headerTitle="登录事件"
toolBarRender={() => [
<Tooltip key="delete" title="删除所有事件">
<a
<Button
onClick={handleDeleteAllEvents}
className="text-red-500 hover:text-red-600"
danger={true}
>
删除所有事件
</a>
</Button>
</Tooltip>,
]}
locale={{
+488 -456
View File
File diff suppressed because it is too large. Load diff