feat: Add settings to prevent certificate leakage. (#10785)

Refs https://github.com/1Panel-dev/1Panel/issues/10755
This commit is contained in:
CityFun
2025-10-27 10:06:17 +00:00
committed by GitHub
parent 606d3c1717
commit db002ec915
28 changed files with 172 additions and 71 deletions
+2 -2
View File
@@ -161,13 +161,13 @@ func (b *BaseApi) GetNginxModules(c *gin.Context) {
// @Tags OpenResty
// @Summary Operate default HTTPs
// @Accept json
// @Param request body request.NginxOperateReq true "request"
// @Param request body request.NginxDefaultHTTPSUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /openresty/https [post]
func (b *BaseApi) OperateDefaultHTTPs(c *gin.Context) {
var req request.NginxOperateReq
var req request.NginxDefaultHTTPSUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
+5
View File
@@ -130,3 +130,8 @@ type NginxModuleUpdate struct {
type NginxOperateReq struct {
Operate string `json:"operate" validate:"required,oneof=enable disable"`
}
type NginxDefaultHTTPSUpdate struct {
Operate string `json:"operate" validate:"required,oneof=enable disable"`
SSLRejectHandshake bool `json:"sslRejectHandshake"`
}
+2 -1
View File
@@ -82,5 +82,6 @@ type NginxBuildConfig struct {
}
type NginxConfigRes struct {
Https bool `json:"https"`
Https bool `json:"https"`
SSLRejectHandshake bool `json:"sslRejectHandshake"`
}
+31 -23
View File
@@ -1997,7 +1997,7 @@ func handleOpenrestyFile(appInstall *model.AppInstall) error {
break
}
}
if err := handleSSLConfig(appInstall, hasDefaultWebsite); err != nil {
if err := handleSSLConfig(appInstall, hasDefaultWebsite, true); err != nil {
return err
}
if len(websites) == 0 {
@@ -2026,34 +2026,37 @@ func handleDefaultServer(appInstall *model.AppInstall) error {
return nil
}
func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool) error {
func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRejectHandshake bool) error {
sslDir := path.Join(appInstall.GetPath(), "conf", "ssl")
fileOp := files.NewFileOp()
if !fileOp.Stat(sslDir) {
return errors.New("ssl dir not found")
}
ca, _ := websiteCARepo.GetFirst(repo.WithByName("1Panel"))
if ca.ID == 0 {
global.LOG.Errorf("create openresty default ssl failed ca not found")
return nil
hasDefaultSSL := fileOp.Stat(path.Join(sslDir, "fullchain.pem")) && fileOp.Stat(path.Join(sslDir, "privkey.pem")) && fileOp.Stat(path.Join(sslDir, "root_ssl.conf"))
if !hasDefaultSSL {
ca, _ := websiteCARepo.GetFirst(repo.WithByName("1Panel"))
if ca.ID == 0 {
global.LOG.Errorf("create openresty default ssl failed ca not found")
return nil
}
caService := NewIWebsiteCAService()
caRequest := request.WebsiteCAObtain{
ID: ca.ID,
Domains: "localhost",
KeyType: "4096",
Time: 99,
Unit: "year",
Dir: sslDir,
PushDir: true,
}
websiteSSL, err := caService.ObtainSSL(caRequest)
if err != nil {
return err
}
defer func() {
_ = NewIWebsiteSSLService().Delete([]uint{websiteSSL.ID})
}()
}
caService := NewIWebsiteCAService()
caRequest := request.WebsiteCAObtain{
ID: ca.ID,
Domains: "localhost",
KeyType: "4096",
Time: 99,
Unit: "year",
Dir: sslDir,
PushDir: true,
}
websiteSSL, err := caService.ObtainSSL(caRequest)
if err != nil {
return err
}
defer func() {
_ = NewIWebsiteSSLService().Delete([]uint{websiteSSL.ID})
}()
defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf")
content, err := os.ReadFile(defaultConfigPath)
if err != nil {
@@ -2068,6 +2071,11 @@ func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool) error
updateDefaultServer(defaultServer, appInstall.HttpPort, appInstall.HttpsPort, !hasDefaultWebsite, true)
defaultServer.UpdateDirective("include", []string{"/usr/local/openresty/nginx/conf/ssl/root_ssl.conf"})
defaultServer.UpdateDirective("http2", []string{"on"})
if sslRejectHandshake {
defaultServer.UpdateDirective("ssl_reject_handshake", []string{"on"})
} else {
defaultServer.RemoveDirective("ssl_reject_handshake", []string{})
}
if err = nginx.WriteConfig(defaultConfig, nginx.IndentedStyle); err != nil {
return err
}
+18 -8
View File
@@ -45,7 +45,7 @@ type INginxService interface {
GetModules() (*response.NginxBuildConfig, error)
UpdateModule(req request.NginxModuleUpdate) error
OperateDefaultHTTPs(req request.NginxOperateReq) error
OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) error
GetDefaultHttpsStatus() (*response.NginxConfigRes, error)
}
@@ -354,7 +354,7 @@ func (n NginxService) UpdateModule(req request.NginxModuleUpdate) error {
return fileOp.SaveFileWithByte(moduleConfigPath, moduleByte, constant.DirPerm)
}
func (n NginxService) OperateDefaultHTTPs(req request.NginxOperateReq) error {
func (n NginxService) OperateDefaultHTTPs(req request.NginxDefaultHTTPSUpdate) error {
appInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
@@ -372,11 +372,18 @@ func (n NginxService) OperateDefaultHTTPs(req request.NginxOperateReq) error {
if err != nil {
return err
}
if req.Operate == "enable" {
if err := handleSSLConfig(&appInstall, hasDefaultWebsite); err != nil {
switch req.Operate {
case "enable":
if req.SSLRejectHandshake {
defaultWebsite, _ := websiteRepo.GetFirst(websiteRepo.WithDefaultServer())
if defaultWebsite.ID > 0 {
return buserr.New("ErrDefaultWebsite")
}
}
if err := handleSSLConfig(&appInstall, hasDefaultWebsite, req.SSLRejectHandshake); err != nil {
return err
}
} else if req.Operate == "disable" {
case "disable":
defaultConfig, err := parser.NewStringParser(string(content)).Parse()
if err != nil {
return err
@@ -387,6 +394,7 @@ func (n NginxService) OperateDefaultHTTPs(req request.NginxOperateReq) error {
defaultServer.RemoveListen(fmt.Sprintf("[::]:%d", appInstall.HttpsPort))
defaultServer.RemoveDirective("include", []string{"/usr/local/openresty/nginx/conf/ssl/root_ssl.conf"})
defaultServer.RemoveDirective("http2", []string{"on"})
defaultServer.RemoveDirective("ssl_reject_handshake", []string{"on"})
if err = nginx.WriteConfig(defaultConfig, nginx.IndentedStyle); err != nil {
return err
}
@@ -413,9 +421,11 @@ func (n NginxService) GetDefaultHttpsStatus() (*response.NginxConfigRes, error)
res := &response.NginxConfigRes{}
for _, directive := range defaultServer.GetDirectives() {
if directive.GetName() == "include" && directive.GetParameters()[0] == "/usr/local/openresty/nginx/conf/ssl/root_ssl.conf" {
return &response.NginxConfigRes{
Https: true,
}, nil
res.Https = true
}
if directive.GetName() == "ssl_reject_handshake" && directive.GetParameters()[0] == "on" {
res.Https = true
res.SSLRejectHandshake = true
}
}
return res, nil
+3 -1
View File
@@ -121,9 +121,11 @@ func updateDefaultServerConfig(enable bool) error {
includeSSL := false
for _, dir := range defaultServer.GetDirectives() {
if dir.GetName() == "ssl_reject_handshake" && dir.GetParameters()[0] == "on" {
defaultServer.RemoveDirective("ssl_reject_handshake", []string{"on"})
}
if dir.GetName() == "include" && dir.GetParameters()[0] == "/usr/local/openresty/nginx/conf/ssl/root_ssl.conf" {
includeSSL = true
break
}
}
updateDefaultServer(defaultServer, nginxInstall.HttpPort, nginxInstall.HttpsPort, enable, includeSSL)
+1
View File
@@ -147,6 +147,7 @@ Status: 'Status'
start: 'Start'
stop: 'Stop'
delete: 'Delete'
ErrDefaultWebsite: 'Default website has been set, please cancel it before setting!'
#ssl
ErrSSLCannotDelete: 'The {{ .name }} certificate is being used by a website and cannot be deleted'
+1
View File
@@ -146,6 +146,7 @@ Status: 'Estado'
start: 'Iniciar'
stop: 'Detener'
delete: 'Eliminar'
ErrDefaultWebsite: 'El sitio web predeterminado ya está configurado, ¡cancélelo antes de configurar!'
#ssl
ErrSSLCannotDelete: 'El certificado {{ .name }} está siendo utilizado por un sitio web y no puede eliminarse'
+1
View File
@@ -146,6 +146,7 @@ Status: 'ステータス'
start: '開始'
stop: '停止'
delete: '削除'
ErrDefaultWebsite: 'デフォルト Web サイトが既に設定されています。設定する前にキャンセルしてください!'
#ssl
ErrSSLCannotDelete: '{{ .name }} 証明書は Web サイトで使用されているため、削除できません'
+1
View File
@@ -147,6 +147,7 @@ Status: '상태'
start: '시작'
stop: '중지'
delete: '삭제'
ErrDefaultWebsite: '기본 웹사이트가 이미 설정되었습니다. 설정하기 전에 취소하세요!'
#SSL인증
ErrSSLCannotDelete: '{{ .name }} 인증서는 웹사이트에서 사용 중이므로 삭제할 수 없습니다.'
+1
View File
@@ -150,6 +150,7 @@ Status: 'Status'
start: 'Mulakan'
stop: 'Berhenti'
delete: 'Padam'
ErrDefaultWebsite: 'Laman web lalai telah ditetapkan, sila batalkan sebelum menetapkan!'
#ssl
ErrSSLCannotDelete: 'Sijil {{ .name }} sedang digunakan oleh tapak web dan tidak boleh dipadamkan'
+1
View File
@@ -150,6 +150,7 @@ Status: 'Status'
start: 'Iniciar'
stop: 'Parar'
delete: 'Excluir'
ErrDefaultWebsite: 'O site padrão já foi definido, cancele-o antes de definir!'
#ssl
ErrSSLCannotDelete: 'O certificado {{ .name }} está sendo usado por um site e não pode ser excluído'
+1
View File
@@ -150,6 +150,7 @@ Status: 'Статус'
start: 'Запустить'
stop: 'Остановить'
delete: 'Удалить'
ErrDefaultWebsite: 'Веб-сайт по умолчанию уже установлен, отмените его перед настройкой!'
#ssl
ErrSSLCannotDelete: 'Сертификат {{ .name }} используется веб-сайтом и не может быть удален'
+1
View File
@@ -146,6 +146,7 @@ Status: '狀態'
start: '開啟'
stop: '關閉'
delete: '刪除'
ErrDefaultWebsite: '已經設置默認網站,請取消後再設置!'
#ssl
ErrSSLCannotDelete: '{{ .name }} 憑證正在被網站使用,無法刪除'
+1
View File
@@ -146,6 +146,7 @@ Status: '状态'
start: '开启'
stop: '关闭'
delete: '删除'
ErrDefaultWebsite: "已经设置默认网站,请取消后再设置!"
#ssl
ErrSSLCannotDelete: "{{ .name }} 证书正在被网站使用,无法删除"
+5
View File
@@ -53,9 +53,14 @@ export namespace Nginx {
export interface NginxHttpsStatus {
https: boolean;
sslRejectHandshake: boolean;
}
export interface NginxOperateReq {
operate: string;
}
export interface NginxHttpsOperateReq extends NginxOperateReq {
sslRejectHandshake: boolean;
}
}
+3
View File
@@ -2731,6 +2731,9 @@ const message = {
'Click build after adding/modifying a module. OpenResty will automatically restart upon successful build.',
defaultHttps: 'HTTPS Anti-tampering',
defaultHttpsHelper1: 'Enabling this can resolve HTTPS tampering issues.',
sslRejectHandshake: 'Reject default SSL handshake',
sslRejectHandshakeHelper:
'Enabling this can avoid certificate leakage, setting a default website will invalidate this setting',
},
ssl: {
create: 'Request',
+3
View File
@@ -2709,6 +2709,9 @@ const message = {
'Haz clic en compilar después de agregar/modificar un módulo. OpenResty se reiniciará automáticamente tras una compilación exitosa.',
defaultHttps: 'HTTPS Anti-manipulación',
defaultHttpsHelper1: 'Habilitar esto puede resolver problemas de manipulación de HTTPS.',
sslRejectHandshake: 'Rechazar handshake SSL predeterminado',
sslRejectHandshakeHelper:
'Habilitar esto puede evitar la fuga de certificados, establecer un sitio web predeterminado invalidará esta configuración',
},
ssl: {
create: 'Solicitar',
+3
View File
@@ -2648,6 +2648,9 @@ const message = {
'モジュールの追加/変更後にビルドをクリックしますビルドが成功するとOpenRestyは自動的に再起動します',
defaultHttps: 'HTTPS 改ざん防止',
defaultHttpsHelper1: 'これを有効にするとHTTPS 改ざん問題を解決できます',
sslRejectHandshake: 'デフォルト SSL ハンドシェイクを拒否',
sslRejectHandshakeHelper:
'有効にすると証明書の漏洩を防げますがデフォルト Web サイトを設定するとこの設定は無効になります',
},
ssl: {
create: 'リクエスト',
+3
View File
@@ -2600,6 +2600,9 @@ const message = {
buildHelper: '모듈 추가/수정 후 빌드를 클릭하세요. 빌드가 성공하면 OpenResty가 자동으로 재시작됩니다.',
defaultHttps: 'HTTPS 변조 방지',
defaultHttpsHelper1: '이를 활성화하면 HTTPS 변조 문제를 해결할 수 있습니다.',
sslRejectHandshake: '기본 SSL 핸드셰이크 거부',
sslRejectHandshakeHelper:
'활성화하면 인증서 누출을 방지할 수 있지만, 기본 웹사이트를 설정하면 이 설정이 무효화됩니다',
},
ssl: {
create: '요청',
+3
View File
@@ -2711,6 +2711,9 @@ const message = {
'Klik Bina selepas menambah/mengubah suai modul. Pembinaan yang berjaya akan memulakan semula OpenResty secara automatik.',
defaultHttps: 'HTTPS Anti-tampering',
defaultHttpsHelper1: 'Mengaktifkan ini dapat menyelesaikan masalah tampering HTTPS.',
sslRejectHandshake: 'Tolak jabat tangan SSL lalai',
sslRejectHandshakeHelper:
'Mengaktifkan ini boleh mengelakkan kebocoran sijil, menetapkan laman web lalai akan membatalkan tetapan ini',
},
ssl: {
create: 'Permintaan',
+3
View File
@@ -2715,6 +2715,9 @@ const message = {
'Clique em Construir após adicionar/modificar um módulo. Construção bem-sucedida reiniciará automaticamente o OpenResty.',
defaultHttps: 'HTTPS Anti-tampering',
defaultHttpsHelper1: 'A ativação desta opção pode resolver problemas de adulteração HTTPS.',
sslRejectHandshake: 'Rejeitar handshake SSL padrão',
sslRejectHandshakeHelper:
'Ativar isso pode evitar vazamento de certificados, definir um site padrão invalidará esta configuração',
},
ssl: {
create: 'Solicitar',
+3
View File
@@ -2712,6 +2712,9 @@ const message = {
'Нажмите Сборка после добавления/изменения модуля. Успешная сборка автоматически перезапустит OpenResty.',
defaultHttps: 'HTTPS Анти-вмешательство',
defaultHttpsHelper1: 'Включение этого параметра может решить проблему вмешательства в HTTPS.',
sslRejectHandshake: 'Отклонить стандартное SSL-рукопожатие',
sslRejectHandshakeHelper:
'Включение этого может предотвратить утечку сертификатов, установка веб-сайта по умолчанию сделает эту настройку недействительной',
},
ssl: {
create: 'Запросить',
+3
View File
@@ -2768,6 +2768,9 @@ const message = {
'Modül ekledikten/düzenledikten sonra oluştura tıklayın. OpenResty, başarılı oluşturma üzerine otomatik olarak yeniden başlatılacaktır.',
defaultHttps: 'HTTPS Anti-sızdırma',
defaultHttpsHelper1: 'Bu özelliği etkinleştirerek HTTPS sızdırma sorunlarını çözebilirsiniz.',
sslRejectHandshake: 'Varsayılan SSL el sıkışmasını reddet',
sslRejectHandshakeHelper:
'Etkinleştirilmesi sertifika sızıntısını önleyebilir, varsayılan bir web sitesi ayarlamak bu ayarı geçersiz kılar',
},
ssl: {
create: 'İstek',
+2
View File
@@ -2540,6 +2540,8 @@ const message = {
buildHelper: '新增/修改模組後點擊構建構建成功後會自動重啟 OpenResty',
defaultHttps: 'HTTPS 防竄站',
defaultHttpsHelper1: '開啟後可以解決 HTTPS 竄站問題',
sslRejectHandshake: '拒絕默認 SSL 握手',
sslRejectHandshakeHelper: '開啟之後可以避免證書洩露設置默認網站會讓此設置失效',
},
ssl: {
create: '申請證書',
+2
View File
@@ -2532,6 +2532,8 @@ const message = {
buildHelper: '添加/修改模块之后点击构建构建成功后会自动重启 OpenResty',
defaultHttps: 'HTTPS 防窜站',
defaultHttpsHelper1: '开启后可以解决 HTTPS 窜站问题',
sslRejectHandshake: '拒绝默认 SSL 握手',
sslRejectHandshakeHelper: '开启之后可以避免证书泄露设置默认网站会让此设置失效',
},
ssl: {
create: '申请证书',
@@ -0,0 +1,61 @@
<template>
<el-row :gutter="20" v-loading="loading">
<el-col :xs="24" :sm="18" :md="8" :lg="8" :xl="8">
<el-form ref="nginxForm" label-position="right" label-width="180px" :model="form" :rules="rules">
<el-form-item :label="$t('commons.table.status')" prop="enable">
<el-switch v-model="enable"></el-switch>
<span class="input-help">{{ $t('nginx.defaultHttpsHelper1') }}</span>
</el-form-item>
<el-form-item :label="$t('nginx.sslRejectHandshake')" prop="sslRejectHandshake">
<el-switch v-model="form.sslRejectHandshake"></el-switch>
<span class="input-help">{{ $t('nginx.sslRejectHandshakeHelper') }}</span>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit()" :disabled="loading">
{{ $t('commons.button.save') }}
</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import { getHttpsStatus, operateHttps } from '@/api/modules/nginx';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
const showPage = ref(false);
const enable = ref(false);
const loading = ref(false);
const form = reactive({
operate: 'enable',
sslRejectHandshake: false,
});
const getStatus = async () => {
try {
const res = await getHttpsStatus();
enable.value = res.data.https;
form.sslRejectHandshake = res.data.sslRejectHandshake;
showPage.value = true;
} catch {}
};
const submit = async () => {
loading.value = true;
try {
form.operate = enable.value ? 'enable' : 'disable';
await operateHttps(form);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
} catch (error) {
getStatus();
} finally {
loading.value = false;
}
};
onMounted(() => {
getStatus();
});
</script>
@@ -1,43 +1,15 @@
<template>
<div v-if="showPage" v-loading="loading">
<el-text>{{ $t('nginx.defaultHttps') }}</el-text>
<el-switch class="ml-2" v-model="enable" @change="changeStatus"></el-switch>
<div>
<el-text type="info" size="small">{{ $t('nginx.defaultHttpsHelper1') }}</el-text>
</div>
<div>
<el-tabs tab-position="left" v-model="tabIndex" ref="tabsRef">
<el-tab-pane :label="$t('nginx.defaultHttps')" name="0">
<Https v-if="tabIndex == '0'"></Https>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script lang="ts" setup>
import { getHttpsStatus, operateHttps } from '@/api/modules/nginx';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import Https from './https/index.vue';
const showPage = ref(false);
const enable = ref(false);
const loading = ref(false);
const getStatus = async () => {
try {
const res = await getHttpsStatus();
enable.value = res.data.https;
showPage.value = true;
} catch {}
};
const changeStatus = async () => {
loading.value = true;
try {
await operateHttps({ operate: enable.value ? 'enable' : 'disable' });
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
} catch (error) {
enable.value = !enable.value;
} finally {
loading.value = false;
}
};
onMounted(() => {
getStatus();
});
const tabIndex = ref('0');
</script>