feat(system-security): Support Hot Reloading of System Certificates (#7152)
Some checks failed
Build Test / build-linux-binary (push) Has been cancelled
Build / SonarCloud (push) Has been cancelled
sync2gitee / repo-sync (push) Has been cancelled

Refs https://github.com/1Panel-dev/1Panel/issues/7129
This commit is contained in:
zhengkunwang 2024-11-22 17:48:13 +08:00 committed by GitHub
parent 7fdb0a5078
commit c3565f72c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 89 additions and 117 deletions

View File

@ -381,6 +381,9 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
logger.Println(i18n.GetMsgByKey("ExecShellSuccess"))
}
}
reloadSystemSSL(websiteSSL, logger)
return websiteSSL, nil
}

View File

@ -3,6 +3,7 @@ package service
import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
@ -188,6 +189,31 @@ func printSSLLog(logger *log.Logger, msgKey string, params map[string]interface{
logger.Println(i18n.GetMsgWithMap(msgKey, params))
}
func reloadSystemSSL(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
systemSSLEnable, sslID := GetSystemSSL()
if systemSSLEnable && sslID == websiteSSL.ID {
fileOp := files.NewFileOp()
certPath := path.Join(global.CONF.System.BaseDir, "1panel/secret/server.crt")
keyPath := path.Join(global.CONF.System.BaseDir, "1panel/secret/server.key")
printSSLLog(logger, "StartUpdateSystemSSL", nil, logger == nil)
if err := fileOp.WriteFile(certPath, strings.NewReader(websiteSSL.Pem), 0600); err != nil {
logger.Printf("Failed to update the SSL certificate File for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error())
return
}
if err := fileOp.WriteFile(keyPath, strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil {
logger.Printf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error())
return
}
newCert, err := tls.X509KeyPair([]byte(websiteSSL.Pem), []byte(websiteSSL.PrivateKey))
if err != nil {
logger.Printf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error())
return
}
printSSLLog(logger, "UpdateSystemSSLSuccess", nil, logger == nil)
constant.CertStore.Store(&newCert)
}
}
func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
var (
err error
@ -344,6 +370,8 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
}
printSSLLog(logger, "ApplyWebSiteSSLSuccess", nil, apply.DisableLog)
}
reloadSystemSSL(websiteSSL, logger)
}()
return nil

View File

@ -1002,23 +1002,22 @@ func saveCertificateFile(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
}
}
func GetSystemSSL() (bool, bool, uint) {
func GetSystemSSL() (bool, uint) {
sslSetting, err := settingRepo.Get(settingRepo.WithByKey("SSL"))
if err != nil {
global.LOG.Errorf("load service ssl from setting failed, err: %v", err)
return false, false, 0
return false, 0
}
if sslSetting.Value == "enable" {
sslID, _ := settingRepo.Get(settingRepo.WithByKey("SSLID"))
idValue, _ := strconv.Atoi(sslID.Value)
if idValue <= 0 {
return false, false, 0
return false, 0
}
auto, _ := settingRepo.Get(settingRepo.WithByKey("AutoRestart"))
return true, auto.Value == "enable", uint(idValue)
return true, uint(idValue)
}
return false, false, 0
return false, 0
}
func UpdateSSLConfig(websiteSSL model.WebsiteSSL) error {
@ -1037,22 +1036,7 @@ func UpdateSSLConfig(websiteSSL model.WebsiteSSL) error {
return buserr.WithErr(constant.ErrSSLApply, err)
}
}
enable, auto, sslID := GetSystemSSL()
if enable && sslID == websiteSSL.ID {
fileOp := files.NewFileOp()
secretDir := path.Join(global.CONF.System.BaseDir, "1panel/secret")
if err := fileOp.WriteFile(path.Join(secretDir, "server.crt"), strings.NewReader(websiteSSL.Pem), 0600); err != nil {
global.LOG.Errorf("Failed to update the SSL certificate File for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error())
return err
}
if err := fileOp.WriteFile(path.Join(secretDir, "server.key"), strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil {
global.LOG.Errorf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error())
return err
}
if auto {
_, _ = cmd.Exec("systemctl restart 1panel.service")
}
}
reloadSystemSSL(&websiteSSL, nil)
return nil
}

View File

@ -1,5 +1,7 @@
package constant
import "sync/atomic"
type DBContext string
const (
@ -123,3 +125,5 @@ var DynamicRoutes = []string{
`^/databases/postgresql/setting/[^/]+/[^/]+$`,
`^/websites/[^/]+/config/[^/]+$`,
}
var CertStore atomic.Value

View File

@ -1,8 +1,6 @@
package job
import (
"path"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
@ -10,9 +8,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/service"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files"
)
type ssl struct {
@ -23,7 +19,6 @@ func NewSSLJob() *ssl {
}
func (ssl *ssl) Run() {
systemSSLEnable, auto, sslID := service.GetSystemSSL()
sslRepo := repo.NewISSLRepo()
sslService := service.NewIWebsiteSSLService()
sslList, _ := sslRepo.List()
@ -59,22 +54,6 @@ func (ssl *ssl) Run() {
continue
}
}
if systemSSLEnable && sslID == s.ID {
websiteSSL, _ := sslRepo.GetFirst(repo.NewCommonRepo().WithByID(s.ID))
fileOp := files.NewFileOp()
secretDir := path.Join(global.CONF.System.BaseDir, "1panel/secret")
if err := fileOp.WriteFile(path.Join(secretDir, "server.crt"), strings.NewReader(websiteSSL.Pem), 0600); err != nil {
global.LOG.Errorf("Failed to update the SSL certificate File for 1Panel System domain [%s] , err:%s", s.PrimaryDomain, err.Error())
continue
}
if err := fileOp.WriteFile(path.Join(secretDir, "server.key"), strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil {
global.LOG.Errorf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", s.PrimaryDomain, err.Error())
continue
}
if auto {
_, _ = cmd.Exec("systemctl restart 1panel.service")
}
}
global.LOG.Infof("The SSL certificate for the [%s] domain has been successfully updated", s.PrimaryDomain)
}
}

View File

@ -127,6 +127,8 @@ ErrDefaultCA: "The default organization cannot be deleted"
ApplyWebSiteSSLLog: "Start updating {{ .name }} website certificate"
ErrUpdateWebsiteSSL: "{{ .name }} website failed to update certificate: {{ .err }}"
ApplyWebSiteSSLSuccess: "Update website certificate successfully"
StartUpdateSystemSSL: "Start updating system certificate"
UpdateSystemSSLSuccess: "Update system certificate successfully"
#mysql
ErrUserIsExist: "The current user already exists. Please enter a new user"

View File

@ -126,6 +126,8 @@ ErrDefaultCA: "默認機構不能刪除"
ApplyWebSiteSSLLog: "開始更新 {{ .name }} 網站憑證"
ErrUpdateWebsiteSSL: "{{ .name }} 網站更新憑證失敗: {{ .err }}"
ApplyWebSiteSSLSuccess: "更新網站憑證成功"
StartUpdateSystemSSL: "開始更新系統證書"
UpdateSystemSSLSuccess: "更新系統證書成功"
#mysql

View File

@ -130,6 +130,8 @@ ApplyWebSiteSSLSuccess: "更新网站证书成功"
ErrExecShell: "执行脚本失败 {{ .err }}"
ExecShellStart: "开始执行脚本"
ExecShellSuccess: "脚本执行成功"
StartUpdateSystemSSL: "开始更新系统证书"
UpdateSystemSSLSuccess: "更新系统证书成功"
#mysql
ErrUserIsExist: "当前用户已存在,请重新输入"

View File

@ -4,13 +4,13 @@ import (
"crypto/tls"
"encoding/gob"
"fmt"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/i18n"
"net"
"net/http"
"os"
"path"
"github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/init/app"
"github.com/1Panel-dev/1Panel/backend/init/business"
@ -81,12 +81,16 @@ func Start() {
if err != nil {
panic(err)
}
constant.CertStore.Store(&cert)
server.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return constant.CertStore.Load().(*tls.Certificate), nil
},
}
global.LOG.Infof("listen at https://%s:%s [%s]", global.CONF.System.BindAddress, global.CONF.System.Port, tcpItem)
if err := server.ServeTLS(tcpKeepAliveListener{ln.(*net.TCPListener)}, certPath, keyPath); err != nil {
if err := server.ServeTLS(tcpKeepAliveListener{ln.(*net.TCPListener)}, "", ""); err != nil {
panic(err)
}
} else {

View File

@ -29,7 +29,6 @@ export namespace Setting {
bindAddress: string;
ssl: string;
sslType: string;
autoRestart: string;
allowIPs: string;
bindDomain: string;
securityEntrance: string;

View File

@ -1551,11 +1551,6 @@ const message = {
bindDomain: 'Bind Domain',
unBindDomain: 'Unbind domain',
panelSSL: 'Panel SSL',
sslAutoRestart: 'Restart 1Panel service after certificate auto-renewal',
sslChangeHelper1:
'Currently, automatic restart of 1Panel service is not selected. The certificate auto-renewal will not take effect immediately and will still require a manual restart of 1Panel.',
sslChangeHelper2:
'The 1Panel service will automatically restart after setting the panel SSL. Do you want to continue?',
unBindDomainHelper:
'The action of unbinding a domain name may cause system insecurity. Do you want to continue?',
bindDomainHelper:

View File

@ -1494,9 +1494,6 @@ const message = {
bindDomain: '域名綁定',
unBindDomain: '域名解綁',
panelSSL: '面板 SSL',
sslAutoRestart: '證書自動續期後重啟 1Panel 服務',
sslChangeHelper1: '當前未勾選自動重啟 1Panel 服務證書自動續期後不會立即生效仍需手動重啟 1Panel',
sslChangeHelper2: '設置面板 SSL 後將自動重啟 1Panel 服務是否繼續',
unBindDomainHelper: '解除域名綁定可能造成系統不安全是否繼續',
bindDomainHelper: '設置域名綁定後僅能通過設置中域名訪問 1Panel 服務',
bindDomainHelper1: '綁定域名為空時則取消域名綁定',

View File

@ -1496,9 +1496,6 @@ const message = {
bindDomain: '域名绑定',
unBindDomain: '域名解绑',
panelSSL: '面板 SSL',
sslAutoRestart: '证书自动续期后重启 1Panel 服务',
sslChangeHelper1: '当前未勾选自动重启 1Panel 服务证书自动续期后不会立即生效仍需手动重启 1Panel',
sslChangeHelper2: '设置面板 SSL 后将自动重启 1Panel 服务是否继续',
unBindDomainHelper: '解除域名绑定可能造成系统不安全是否继续',
bindDomainHelper: '设置域名绑定后仅能通过设置中域名访问 1Panel 服务',
bindDomainHelper1: '绑定域名为空时则取消域名绑定',

View File

@ -34,7 +34,7 @@ const settingRouter = {
hidden: true,
meta: {
requiresAuth: true,
activeMenu: 'Setting',
activeMenu: '/settings',
},
},
{
@ -44,7 +44,7 @@ const settingRouter = {
hidden: true,
meta: {
requiresAuth: true,
activeMenu: 'Setting',
activeMenu: '/settings',
},
},
{
@ -54,7 +54,7 @@ const settingRouter = {
hidden: true,
meta: {
requiresAuth: true,
activeMenu: 'Setting',
activeMenu: '/settings',
},
},
{
@ -64,7 +64,7 @@ const settingRouter = {
hidden: true,
meta: {
requiresAuth: true,
activeMenu: 'Setting',
activeMenu: '/settings',
},
},
{
@ -74,7 +74,7 @@ const settingRouter = {
component: () => import('@/views/setting/snapshot/index.vue'),
meta: {
requiresAuth: true,
activeMenu: 'Setting',
activeMenu: '/settings',
},
},
{
@ -84,7 +84,7 @@ const settingRouter = {
component: () => import('@/views/setting/expired.vue'),
meta: {
requiresAuth: true,
activeMenu: 'Expired',
activeMenu: '/settings',
},
},
],

View File

@ -225,7 +225,6 @@ const form = reactive({
bindAddress: '',
ssl: 'disable',
sslType: 'self',
autoRestart: 'disable',
securityEntrance: '',
expirationDays: 0,
expirationTime: '',
@ -250,7 +249,6 @@ const search = async () => {
if (form.ssl === 'enable') {
loadInfo();
}
form.autoRestart = res.data.autoRestart;
form.securityEntrance = res.data.securityEntrance;
form.expirationDays = Number(res.data.expirationDays);
form.expirationTime = res.data.expirationTime;
@ -330,7 +328,6 @@ const handleSSL = async () => {
ssl: form.ssl,
sslType: form.sslType,
sslInfo: sslInfo.value,
autoRestart: form.autoRestart,
};
sslRef.value!.acceptParams(params);
return;

View File

@ -112,14 +112,6 @@
</el-descriptions-item>
</el-descriptions>
</div>
<el-form-item v-if="form.sslType !== 'import'">
<el-checkbox true-value="enable" false-value="disable" v-model="form.autoRestart">
{{ $t('setting.sslAutoRestart') }}
</el-checkbox>
<span v-if="form.autoRestart === 'disable'" class="input-help">
{{ $t('setting.sslChangeHelper1') }}
</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
@ -143,7 +135,7 @@ import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { downloadSSL, updateSSL } from '@/api/modules/setting';
import { Rules } from '@/global/form-rules';
import { ElMessageBox, FormInstance } from 'element-plus';
import { FormInstance } from 'element-plus';
import { Setting } from '@/api/interface/setting';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { GlobalStore } from '@/store';
@ -162,7 +154,6 @@ const form = reactive({
key: '',
rootPath: '',
timeout: '',
autoRestart: 'disable',
});
const rules = reactive({
@ -179,7 +170,6 @@ const itemSSL = ref();
interface DialogProps {
sslType: string;
sslInfo?: Setting.SSLInfo;
autoRestart: string;
}
const acceptParams = async (params: DialogProps): Promise<void> => {
if (params.sslType.indexOf('-') !== -1) {
@ -202,7 +192,6 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
} else {
loadSSLs();
}
form.autoRestart = params.autoRestart;
drawerVisible.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
@ -243,41 +232,31 @@ const onSaveSSL = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
let msg = !form.autoRestart
? i18n.global.t('setting.sslChangeHelper1') + '\n\n\n' + 'qwdqwdqwd'
: i18n.global.t('setting.sslChangeHelper2');
ElMessageBox.confirm(msg, i18n.global.t('setting.panelSSL'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
let itemType = form.sslType;
if (form.sslType === 'import') {
itemType = form.itemSSLType === 'paste' ? 'import-paste' : 'import-local';
}
let param = {
ssl: 'enable',
sslType: itemType,
domain: '',
sslID: form.sslID,
cert: form.cert,
key: form.key,
autoRestart: form.autoRestart,
};
let itemType = form.sslType;
if (form.sslType === 'import') {
itemType = form.itemSSLType === 'paste' ? 'import-paste' : 'import-local';
}
let param = {
ssl: 'enable',
sslType: itemType,
domain: '',
sslID: form.sslID,
cert: form.cert,
key: form.key,
};
let href = window.location.href;
param.domain = href.split('//')[1].split(':')[0];
await updateSSL(param).then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
let href = window.location.href;
param.domain = href.split('//')[1].split(':')[0];
await updateSSL(param).then(() => {
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
let href = window.location.href;
globalStore.isLogin = false;
let address = href.split('://')[1];
if (globalStore.entrance) {
address = address.replaceAll('settings/safe', globalStore.entrance);
} else {
address = address.replaceAll('settings/safe', 'login');
}
window.open(`https://${address}`, '_self');
});
globalStore.isLogin = false;
let address = href.split('://')[1];
if (globalStore.entrance) {
address = address.replaceAll('settings/safe', globalStore.entrance);
} else {
address = address.replaceAll('settings/safe', 'login');
}
window.open(`https://${address}`, '_self');
});
});
};

2
go.mod
View File

@ -15,7 +15,7 @@ require (
github.com/gin-contrib/gzip v0.0.6
github.com/gin-gonic/gin v1.9.1
github.com/glebarez/sqlite v1.10.0
github.com/go-acme/lego/v4 v4.20.2
github.com/go-acme/lego/v4 v4.20.4
github.com/go-gormigrate/gormigrate/v2 v2.1.1
github.com/go-playground/validator/v10 v10.18.0
github.com/go-redis/redis v6.15.9+incompatible

4
go.sum
View File

@ -318,8 +318,8 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
github.com/go-acme/lego/v4 v4.20.2 h1:ZwO3oLZb8fL6up1OZVJP3yHuvqhozzlEmyqKmhrPchQ=
github.com/go-acme/lego/v4 v4.20.2/go.mod h1:foauPlhnhoq8WUphaWx5U04uDc+JGhk4ZZtPz/Vqsjg=
github.com/go-acme/lego/v4 v4.20.4 h1:yCQGBX9jOfMbriEQUocdYm7EBapdTp8nLXYG8k6SqSU=
github.com/go-acme/lego/v4 v4.20.4/go.mod h1:foauPlhnhoq8WUphaWx5U04uDc+JGhk4ZZtPz/Vqsjg=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=