feat: 配置修改后重启,适配 SELinux 策略

This commit is contained in:
ssongliu 2023-05-17 14:45:48 +08:00 committed by zhengkunwang223
parent efd545882f
commit 7452cc19e0
11 changed files with 210 additions and 143 deletions

View file

@ -50,6 +50,33 @@ func (b *BaseApi) UpdateSSH(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
// @Tags SSH
// @Summary Update host ssh setting by file
// @Description 上传文件更新 SSH 配置
// @Accept json
// @Param request body dto.SSHConf true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /host/conffile/update [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFuntions":[],"formatZH":"修改 SSH 配置文件","formatEN":"update SSH conf"}
func (b *BaseApi) UpdateSSHByfile(c *gin.Context) {
var req dto.SSHConf
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := sshService.UpdateByFile(req.File); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags SSH
// @Summary Generate host ssh secret
// @Description 生成 ssh 密钥

View file

@ -20,6 +20,9 @@ type GenerateLoad struct {
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
}
type SSHConf struct {
File string `json:"file"`
}
type SearchSSHLog struct {
PageInfo
Info string `json:"info"`
@ -33,7 +36,8 @@ type SSHLog struct {
}
type SSHHistory struct {
Date time.Time `json:"date"`
Belong string `json:"belong"`
DateStr string `json:"dateStr"`
IsLocal bool `json:"isLocal"`
User string `json:"user"`
AuthMode string `json:"authMode"`
Address string `json:"address"`

View file

@ -2,6 +2,7 @@ package service
import (
"fmt"
"net"
"os"
"os/user"
"path"
@ -13,6 +14,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files"
)
@ -22,6 +24,7 @@ type SSHService struct{}
type ISSHService interface {
GetSSHInfo() (*dto.SSHInfo, error)
UpdateByFile(value string) error
Update(key, value string) error
GenerateSSH(req dto.GenerateSSH) error
LoadSSHSecret(mode string) (string, error)
@ -87,6 +90,36 @@ func (u *SSHService) Update(key, value string) error {
if _, err = file.WriteString(strings.Join(newFiles, "\n")); err != nil {
return err
}
sudo := ""
hasSudo := cmd.HasNoPasswordSudo()
if hasSudo {
sudo = "sudo"
}
if key == "Port" {
stdout, _ := cmd.Exec("getenforce")
if stdout == "Enforcing\n" {
_, _ = cmd.Execf("%s semanage port -a -t ssh_port_t -p tcp %s", sudo, value)
}
}
_, _ = cmd.Execf("%s systemctl restart sshd", sudo)
return nil
}
func (u *SSHService) UpdateByFile(value string) error {
file, err := os.OpenFile(sshPath, os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
return err
}
defer file.Close()
if _, err = file.WriteString(value); err != nil {
return err
}
sudo := ""
hasSudo := cmd.HasNoPasswordSudo()
if hasSudo {
sudo = "sudo"
}
_, _ = cmd.Execf("%s systemctl restart sshd", sudo)
return nil
}
@ -198,6 +231,15 @@ func (u *SSHService) LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) {
}
data.SuccessfulCount = data.TotalCount - data.FailedCount
timeNow := time.Now()
nyc, _ := time.LoadLocation(common.LoadTimeZone())
for i := 0; i < len(data.Logs); i++ {
data.Logs[i].IsLocal = isPrivateIP(net.ParseIP(data.Logs[i].Address))
data.Logs[i].Date, _ = time.ParseInLocation("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s", timeNow.Year(), data.Logs[i].DateStr), nyc)
if data.Logs[i].Date.After(timeNow) {
data.Logs[i].Date = data.Logs[i].Date.AddDate(-1, 0, 0)
}
}
sort.Slice(data.Logs, func(i, j int) bool {
return data.Logs[i].Date.After(data.Logs[j].Date)
})
@ -249,27 +291,22 @@ func updateSSHConf(oldFiles []string, param string, value interface{}) []string
func loadSuccessDatas(command string) []dto.SSHHistory {
var datas []dto.SSHHistory
timeNow := time.Now()
stdout2, err := cmd.Exec(command)
if err == nil {
lines := strings.Split(string(stdout2), "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(parts) != 14 {
if len(parts) < 14 {
continue
}
historyItem := dto.SSHHistory{
Belong: parts[3],
DateStr: fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]),
AuthMode: parts[6],
User: parts[8],
Address: parts[10],
Port: parts[12],
Status: constant.StatusSuccess,
}
historyItem.Date, _ = time.Parse("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s %s %s", timeNow.Year(), parts[0], parts[1], parts[2]))
if historyItem.Date.After(timeNow) {
historyItem.Date = historyItem.Date.AddDate(-1, 0, 0)
}
datas = append(datas, historyItem)
}
}
@ -278,29 +315,24 @@ func loadSuccessDatas(command string) []dto.SSHHistory {
func loadFailedAuthDatas(command string) []dto.SSHHistory {
var datas []dto.SSHHistory
timeNow := time.Now()
stdout2, err := cmd.Exec(command)
if err == nil {
lines := strings.Split(string(stdout2), "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(parts) != 15 {
if len(parts) < 14 {
continue
}
historyItem := dto.SSHHistory{
Belong: parts[3],
DateStr: fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]),
AuthMode: parts[8],
User: parts[10],
Address: parts[11],
Port: parts[13],
Status: constant.StatusFailed,
}
historyItem.Date, _ = time.Parse("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s %s %s", timeNow.Year(), parts[0], parts[1], parts[2]))
if historyItem.Date.After(timeNow) {
historyItem.Date = historyItem.Date.AddDate(-1, 0, 0)
}
if strings.Contains(line, ": ") {
historyItem.Message = strings.Split(line, ": ")[0]
historyItem.Message = strings.Split(line, ": ")[1]
}
datas = append(datas, historyItem)
}
@ -310,29 +342,24 @@ func loadFailedAuthDatas(command string) []dto.SSHHistory {
func loadFailedSecureDatas(command string) []dto.SSHHistory {
var datas []dto.SSHHistory
timeNow := time.Now()
stdout2, err := cmd.Exec(command)
if err == nil {
lines := strings.Split(string(stdout2), "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(parts) != 14 {
if len(parts) < 14 {
continue
}
historyItem := dto.SSHHistory{
Belong: parts[3],
DateStr: fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]),
AuthMode: parts[6],
User: parts[8],
Address: parts[10],
Port: parts[12],
Status: constant.StatusFailed,
}
historyItem.Date, _ = time.Parse("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s %s %s", timeNow.Year(), parts[0], parts[1], parts[2]))
if historyItem.Date.After(timeNow) {
historyItem.Date = historyItem.Date.AddDate(-1, 0, 0)
}
if strings.Contains(line, ": ") {
historyItem.Message = strings.Split(line, ": ")[0]
historyItem.Message = strings.Split(line, ": ")[1]
}
datas = append(datas, historyItem)
}
@ -346,3 +373,16 @@ func handleGunzip(path string) error {
}
return nil
}
func isPrivateIP(ip net.IP) bool {
if ip4 := ip.To4(); ip4 != nil {
switch true {
case ip4[0] == 10:
return true
case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31:
return true
case ip4[0] == 192 && ip4[1] == 168:
return true
}
}
return false
}

View file

@ -1,98 +0,0 @@
package service
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"testing"
"time"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
)
func TestCa(t *testing.T) {
var (
fileList []string
datas []history
successfulCount int
failedCount int
)
baseDir := "/Users/slooop/Downloads"
if err := filepath.Walk(baseDir, func(pathItem string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth") {
fileList = append(fileList, strings.ReplaceAll(pathItem, ".gz", ""))
}
return nil
}); err != nil {
fmt.Println(err)
}
for i := 0; i < len(fileList); i++ {
if strings.HasPrefix(path.Base(fileList[i]), "secure") {
dataItem := loadDatas2(fmt.Sprintf("cat %s | grep -a 'Failed password for' | grep -v 'invalid'", fileList[i]), 14, constant.StatusFailed)
failedCount += len(dataItem)
datas = append(datas, dataItem...)
}
if strings.HasPrefix(path.Base(fileList[i]), "auth.log") {
dataItem := loadDatas2(fmt.Sprintf("cat %s | grep -a 'Connection closed by authenticating user' | grep -a 'preauth'", fileList[i]), 15, constant.StatusFailed)
failedCount += len(dataItem)
datas = append(datas, dataItem...)
}
dataItem := loadDatas2(fmt.Sprintf("cat %s | grep Accepted", fileList[i]), 14, constant.StatusSuccess)
datas = append(datas, dataItem...)
}
successfulCount = len(datas) - failedCount
fmt.Println(len(datas), successfulCount, failedCount)
}
func loadDatas2(command string, length int, status string) []history {
var datas []history
stdout2, err := cmd.Exec(command)
if err == nil {
lines := strings.Split(string(stdout2), "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(parts) != length {
continue
}
historyItem := history{
Belong: parts[3],
User: parts[8],
AuthMode: parts[6],
Address: parts[10],
Port: parts[12],
Status: status,
}
dateStr := fmt.Sprintf("%d %s %s %s", time.Now().Year(), parts[0], parts[1], parts[2])
historyItem.Date, _ = time.Parse("2006 Jan 2 15:04:05", dateStr)
// if err != nil {
// historyItem.Date, _ = time.Parse("2006 Jan 2 15:04:05", dateStr)
// }
fmt.Println(dateStr + "===>" + historyItem.Date.Format("2006.01.02 15:04:05"))
datas = append(datas, historyItem)
}
}
return datas
}
func TestCas(t *testing.T) {
ss := "2023 May 9 14:48:28"
kk, err := time.Parse("2006 Jan 2 15:04:05", ss)
fmt.Println(kk, err)
}
type history struct {
Date time.Time
Belong string
User string
AuthMode string
Address string
Port string
Status string
Message string
}

View file

@ -40,6 +40,7 @@ func (s *HostRouter) InitHostRouter(Router *gin.RouterGroup) {
hostRouter.POST("/ssh/generate", baseApi.GenerateSSH)
hostRouter.POST("/ssh/secret", baseApi.LoadSSHSecret)
hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs)
hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile)
hostRouter.GET("/command", baseApi.ListCommand)
hostRouter.POST("/command", baseApi.CreateCommand)

View file

@ -129,7 +129,7 @@ export namespace Host {
}
export interface sshHistory {
date: Date;
belong: string;
isLocal: boolean;
user: string;
authMode: string;
address: string;

View file

@ -104,6 +104,9 @@ export const getSSHInfo = () => {
export const updateSSH = (key: string, value: string) => {
return http.post(`/hosts/ssh/update`, { key: key, value: value });
};
export const updateSSHByfile = (file: string) => {
return http.post(`/hosts/ssh/conffile/update`, { file: file });
};
export const generateSecret = (params: Host.SSHGenerate) => {
return http.post(`/hosts/ssh/generate`, params);
};

View file

@ -815,6 +815,45 @@ const message = {
'The default user of the PHP operating environment: the user group is 1000:1000, it is normal that the users inside and outside the container show inconsistencies',
searchHelper: 'Support wildcards such as *',
},
ssh: {
sshChange: 'SSH Setting',
sshChangeHelper: 'Are you sure to change the SSH {0} configuration to {1}?',
sshFileChangeHelper:
'Modifying the configuration file may cause service availability. Exercise caution when performing this operation. Do you want to continue?',
port: 'Port',
portHelper: 'Specifies the port number monitored by the SSH service. The default port number is 22.',
listenAddress: 'Listening address',
addressHelper:
'Specify the IP address monitored by the SSH service. The default value is 0.0.0.0. That is, all network interfaces are monitored.',
permitRootLogin: 'root user',
rootSettingHelper: 'The default login mode is SSH for user root.',
rootHelper1: 'Allow SSH login',
rootHelper2: 'Disable SSH login',
rootHelper3: 'Only key login is allowed',
rootHelper4: 'Only predefined commands can be executed. No other operations can be performed.',
passwordAuthentication: 'Password auth',
pwdAuthHelper: 'Whether to enable password authentication. This parameter is enabled by default.',
pubkeyAuthentication: 'Key auth',
key: 'Key',
pubkey: 'Key info',
encryptionMode: 'Encryption mode',
passwordHelper: 'Please enter a 6-10 digit encryption password',
generate: 'Generate key',
reGenerate: 'Regenerate key',
keyAuthHelper: 'Whether to enable key authentication. This parameter is enabled by default.',
useDNS: 'useDNS',
dnsHelper:
'Controls whether the DNS resolution function is enabled on the SSH server to verify the identity of the connection.',
loginLogs: 'SSH login log',
loginUser: 'User',
loginMode: 'Login mode',
authenticating: 'Key',
publickey: 'Key',
password: 'Password',
belong: 'Belong',
local: 'Local',
remote: 'Remote',
},
setting: {
all: 'All',
panel: 'Panel',

View file

@ -823,6 +823,7 @@ const message = {
ssh: {
sshChange: 'SSH 配置修改',
sshChangeHelper: '确认将 SSH {0} 配置修改为 {1} ',
sshFileChangeHelper: '直接修改配置文件可能会导致服务不可用请谨慎操作是否继续',
port: '连接端口',
portHelper: '指定 SSH 服务监听的端口号默认为 22',
listenAddress: '监听地址',
@ -832,7 +833,7 @@ const message = {
rootHelper1: '允许 SSH 登录',
rootHelper2: '禁止 SSH 登录',
rootHelper3: '仅允许密钥登录',
rootHelper4: '仅允许带密码的密钥登录',
rootHelper4: '仅允许执行预先定义的命令不能进行其他操作',
passwordAuthentication: '密码认证',
pwdAuthHelper: '是否启用密码认证默认启用',
pubkeyAuthentication: '密钥认证',
@ -849,7 +850,11 @@ const message = {
loginUser: '用户',
loginMode: '登录方式',
authenticating: '密钥',
publickey: '密钥',
password: '密码',
belong: '归属地',
local: '内网',
remote: '外网',
},
setting: {
all: '全部',

View file

@ -6,8 +6,10 @@
<template #toolbar>
<el-row>
<el-col :span="16">
<el-tag type="success">{{ $t('commons.status.success') }} {{ successfulCount }}</el-tag>
<el-tag type="danger" style="margin-left: 5px">
<el-tag type="success" class="tagClass" @click="onSearch('Success')">
{{ $t('commons.status.success') }} {{ successfulCount }}
</el-tag>
<el-tag type="danger" class="tagClass" @click="onSearch('Failed')" style="margin-left: 5px">
{{ $t('commons.status.failed') }} {{ faliedCount }}
</el-tag>
</el-col>
@ -16,8 +18,8 @@
<div class="search-button">
<el-input
v-model="searchInfo"
clearable
@clear="search()"
clearable
suffix-icon="Search"
@keyup.enter="search()"
@change="search()"
@ -29,7 +31,7 @@
</template>
<template #search>
<el-select v-model="searchStatus" @change="search()" clearable>
<el-select v-model="searchStatus" @change="search()">
<template #prefix>{{ $t('commons.table.status') }}</template>
<el-option :label="$t('commons.table.all')" value="All"></el-option>
<el-option :label="$t('commons.status.success')" value="Success"></el-option>
@ -38,9 +40,11 @@
</template>
<template #main>
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search">
<el-table-column min-width="40" :label="$t('logs.loginIP')" prop="ip">
<template #default="{ row }">{{ row.address }}:{{ row.port }}</template>
<el-table-column min-width="60" :label="$t('logs.loginIP')" prop="address" />
<el-table-column min-width="30" :label="$t('ssh.belong')" prop="isLocal">
<template #default="{ row }">{{ row.isLocal ? $t('ssh.local') : $t('ssh.remote') }}</template>
</el-table-column>
<el-table-column min-width="40" :label="$t('firewall.port')" prop="port" />
<el-table-column min-width="40" :label="$t('ssh.loginMode')" prop="authMode">
<template #default="{ row }">{{ $t('ssh.' + row.authMode) }}</template>
</el-table-column>
@ -104,14 +108,33 @@ const search = async () => {
data.value = res.data.logs || [];
faliedCount.value = res.data.failedCount;
successfulCount.value = res.data.successfulCount;
paginationConfig.total = res.data.failedCount + res.data.successfulCount;
if (searchStatus.value === 'Success') {
paginationConfig.total = res.data.successfulCount;
}
if (searchStatus.value === 'Failed') {
paginationConfig.total = res.data.failedCount;
}
if (searchStatus.value === 'All') {
paginationConfig.total = res.data.failedCount + res.data.successfulCount;
}
})
.catch(() => {
loading.value = false;
});
};
const onSearch = (status: string) => {
searchStatus.value = status;
search();
};
onMounted(() => {
search();
});
</script>
<style scoped lang="scss">
.tagClass {
cursor: pointer;
}
</style>

View file

@ -117,8 +117,8 @@ import { oneDark } from '@codemirror/theme-one-dark';
import PubKey from '@/views/host/ssh/ssh/pubkey/index.vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { getSSHInfo, updateSSH } from '@/api/modules/host';
import { LoadFile, SaveFileContent } from '@/api/modules/files';
import { getSSHInfo, updateSSH, updateSSHByfile } from '@/api/modules/host';
import { LoadFile } from '@/api/modules/files';
import { Rules } from '@/global/form-rules';
import { ElMessageBox, FormInstance } from 'element-plus';
@ -141,15 +141,21 @@ const form = reactive({
});
const onSaveFile = async () => {
loading.value = true;
await SaveFileContent({ path: '/etc/ssh/sshd_config', content: sshConf.value })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
ElMessageBox.confirm(i18n.global.t('ssh.sshFileChangeHelper'), i18n.global.t('ssh.sshChange'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
await updateSSHByfile(sshConf.value)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
};
const onOpenDrawer = () => {
@ -165,7 +171,7 @@ const onSave = async (formEl: FormInstance | undefined, key: string, value: stri
}
ElMessageBox.confirm(
i18n.global.t('ssh.sshChangeHelper', [i18n.global.t('ssh.' + itemKey), value]),
i18n.global.t('ssh.sshChangeHelper', [i18n.global.t('ssh.' + itemKey), changei18n(value)]),
i18n.global.t('ssh.sshChange'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
@ -196,6 +202,23 @@ function callback(error: any) {
}
}
const changei18n = (value: string) => {
switch (value) {
case 'yes':
return i18n.global.t('commons.button.enable');
case 'no':
return i18n.global.t('commons.button.disable');
case 'without-password':
return i18n.global.t('ssh.rootHelper3');
case 'forced-commands-only':
return i18n.global.t('ssh.rootHelper4');
case 'yes':
return i18n.global.t('commons.button.enable');
default:
return value;
}
};
const loadSSHConf = async () => {
const res = await LoadFile({ path: '/etc/ssh/sshd_config' });
sshConf.value = res.data || '';