完成 SSH 登录监控
This commit is contained in:
1 parent
9d14a8cba5
commit
7386c6e8dd
6 files changed
+648
-476
No files matched your search
@@ -42,3 +42,4 @@ agent.yaml
|
||||
playground
|
||||
web/node_modules
|
||||
web/dist
|
||||
deploy.sh
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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`);
|
||||
};
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user