feat: Add support for syncing self-signed certificates and manually u… (#13068)

* feat: Add support for syncing self-signed certificates and manually uploaded certificates to other nodes.

* feat: Add support for syncing self-signed certificates and manually
This commit is contained in:
CityFun
2026-06-17 17:00:15 +08:00
committed by GitHub
parent 46af88e1d0
commit 45d7b30abc
10 changed files with 367 additions and 21 deletions
+25
View File
@@ -210,6 +210,27 @@ func (b *BaseApi) UpdateWebsiteSSL(c *gin.Context) {
helper.Success(c)
}
// @Tags Website SSL
// @Summary Push ssl to nodes
// @Accept json
// @Param request body request.WebsiteSSLPush true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/ssl/push [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"推送证书到节点 [domain]","formatEN":"Push ssl to nodes [domain]"}
func (b *BaseApi) PushWebsiteSSLToNode(c *gin.Context) {
var req request.WebsiteSSLPush
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteSSLService.PushToNode(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website SSL
// @Summary Upload ssl
// @Accept json
@@ -248,6 +269,8 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
var req request.WebsiteSSLFileUpload
req.Description = c.PostForm("description")
req.Nodes = c.PostForm("nodes")
req.PushNode, _ = strconv.ParseBool(c.PostForm("pushNode"))
sslID := c.PostForm("sslID")
if sslID != "" {
req.SSLID, _ = strconv.ParseUint(sslID, 10, 64)
@@ -283,6 +306,8 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
Certificate: string(certificateContent),
Description: req.Description,
SSLID: uint(req.SSLID),
PushNode: req.PushNode,
Nodes: req.Nodes,
}
if err := websiteSSLService.Upload(uploadReq); err != nil {
+13
View File
@@ -126,6 +126,15 @@ type WebsiteSSLUpload struct {
Type string `json:"type" validate:"required,oneof=paste local"`
SSLID uint `json:"sslID"`
Description string `json:"description"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
}
type WebsiteSSLPush struct {
ID uint `json:"id" validate:"required"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
TaskID string `json:"taskID" validate:"required"`
}
type WebsiteCASearch struct {
@@ -157,6 +166,8 @@ type WebsiteCAObtain struct {
Description string `json:"description"`
ExecShell bool `json:"execShell"`
Shell string `json:"shell"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
}
type WebsiteCARenew struct {
@@ -167,4 +178,6 @@ type WebsiteSSLFileUpload struct {
Type string `json:"type"`
Description string `json:"description"`
SSLID uint64 `json:"sslID"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
}
+6
View File
@@ -207,6 +207,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
Description: req.Description,
ExecShell: req.ExecShell,
}
setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes)
if req.ExecShell {
websiteSSL.Shell = req.Shell
}
@@ -384,6 +385,11 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
}
}
reloadSystemSSL(websiteSSL, logger)
if websiteSSL.PushNode {
if err = pushSSLToNode(websiteSSL, logger); err != nil {
return nil, err
}
}
return websiteSSL, nil
}
+115 -16
View File
@@ -24,6 +24,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -104,6 +105,7 @@ type IWebsiteSSLService interface {
Delete(ids []uint) error
Update(update request.WebsiteSSLUpdate) error
Upload(req request.WebsiteSSLUpload) error
PushToNode(req request.WebsiteSSLPush) error
ObtainSSL(apply request.WebsiteSSLApply) error
AutoRenewSSL(id uint) error
SyncForRestart() error
@@ -214,10 +216,7 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
}
websiteSSL.Dir = create.Dir
}
if create.PushNode && global.IsMaster && len(create.Nodes) > 0 {
websiteSSL.PushNode = true
websiteSSL.Nodes = create.Nodes
}
setSSLPushConfig(&websiteSSL, create.PushNode, create.Nodes)
var domains []string
if create.OtherDomains != "" {
@@ -282,6 +281,47 @@ func printSSLLog(logger *log.Logger, msgKey string, params map[string]interface{
logger.Println(i18n.GetMsgWithMap(msgKey, params))
}
func normalizeSSLPushConfig(pushNode bool, nodes string) (bool, string) {
nodes = strings.TrimSpace(nodes)
if !pushNode || nodes == "" {
return false, ""
}
return true, nodes
}
func setSSLPushConfig(websiteSSL *model.WebsiteSSL, pushNode bool, nodes string) {
pushNode, nodes = normalizeSSLPushConfig(pushNode, nodes)
if !global.IsMaster || !xpack.MultiNodeProvider.IsXpack() {
pushNode = false
nodes = ""
}
websiteSSL.PushNode = pushNode
websiteSSL.Nodes = nodes
}
func pushSSLToNode(websiteSSL *model.WebsiteSSL, logger *log.Logger) error {
printSSLLog(logger, "StartPushSSLToNode", nil)
if err := xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil {
printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()})
return err
}
printSSLLog(logger, "PushSSLToNodeSuccess", nil)
return nil
}
func pushSSLToNodeWithNewLogger(websiteSSL *model.WebsiteSSL) error {
if !websiteSSL.PushNode {
return nil
}
logFile, logger := newWebsiteSSLLogger(websiteSSL, false)
if logFile != nil {
defer func() {
_ = logFile.Close()
}()
}
return pushSSLToNode(websiteSSL, logger)
}
func newWebsiteSSLLogger(websiteSSL *model.WebsiteSSL, autoRenew bool) (*os.File, *log.Logger) {
flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
if autoRenew {
@@ -567,12 +607,9 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
}
reloadSystemSSL(websiteSSL, logger)
if websiteSSL.PushNode {
printSSLLog(logger, "StartPushSSLToNode", nil)
if err = xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil {
printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()})
if err = pushSSLToNode(websiteSSL, logger); err != nil {
return
}
printSSLLog(logger, "PushSSLToNodeSuccess", nil)
}
}(logFile, logger)
@@ -740,13 +777,13 @@ func (w WebsiteSSLService) Update(update request.WebsiteSSLUpdate) error {
} else {
updateParams["shell"] = ""
}
if update.PushNode {
updateParams["push_node"] = true
updateParams["nodes"] = update.Nodes
} else {
updateParams["push_node"] = false
updateParams["nodes"] = ""
pushNode, nodes := normalizeSSLPushConfig(update.PushNode, update.Nodes)
if !global.IsMaster || !xpack.MultiNodeProvider.IsXpack() {
pushNode = false
nodes = ""
}
updateParams["push_node"] = pushNode
updateParams["nodes"] = nodes
if websiteSSL.Provider != constant.SelfSigned && websiteSSL.Provider != constant.Manual {
acmeAccount, err := websiteAcmeRepo.GetFirst(repo.WithByID(update.AcmeAccountID))
@@ -797,6 +834,7 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
Description: req.Description,
Status: constant.SSLReady,
}
setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes)
var err error
if req.SSLID > 0 {
websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID))
@@ -804,6 +842,7 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
return err
}
websiteSSL.Description = req.Description
setSSLPushConfig(websiteSSL, req.PushNode, req.Nodes)
}
if req.Type == "local" {
fileOp := files.NewFileOp()
@@ -891,9 +930,69 @@ func (w WebsiteSSLService) Upload(req request.WebsiteSSLUpload) error {
if err := UpdateSSLConfig(*websiteSSL); err != nil {
return err
}
return websiteSSLRepo.Save(websiteSSL)
if err := websiteSSLRepo.Save(websiteSSL); err != nil {
return err
}
return pushSSLToNodeWithNewLogger(websiteSSL)
}
return websiteSSLRepo.Create(context.Background(), websiteSSL)
if err := websiteSSLRepo.Create(context.Background(), websiteSSL); err != nil {
return err
}
return pushSSLToNodeWithNewLogger(websiteSSL)
}
func (w WebsiteSSLService) PushToNode(req request.WebsiteSSLPush) error {
if !global.IsMaster {
return errors.New("only master node can push SSL to nodes")
}
if !xpack.MultiNodeProvider.IsXpack() {
return errors.New("SSL node push is an XPack feature")
}
pushNode, nodes := normalizeSSLPushConfig(req.PushNode, req.Nodes)
if !pushNode {
return errors.New("please select nodes to push SSL")
}
websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
}
if websiteSSL.Provider == constant.FromMaster {
return errors.New("SSL imported from master node can not be pushed")
}
if websiteSSL.Status != constant.SSLReady {
return errors.New("only ready SSL can be pushed")
}
if task.CheckResourceTaskIsExecuting(task.TaskPush, task.TaskScopeWebsite, websiteSSL.ID) {
return buserr.New("TaskIsExecuting")
}
if err := websiteSSLRepo.SaveByMap(websiteSSL, map[string]interface{}{
"push_node": pushNode,
"nodes": nodes,
}); err != nil {
return err
}
websiteSSL.PushNode = pushNode
websiteSSL.Nodes = nodes
pushTask, err := task.NewTaskWithOps(websiteSSL.PrimaryDomain, task.TaskPush, task.TaskScopeWebsite, req.TaskID, websiteSSL.ID)
if err != nil {
return err
}
pushTask.AddSubTask(i18n.GetMsgByKey("StartPushSSLToNode"), func(t *task.Task) error {
t.Log(i18n.GetMsgByKey("StartPushSSLToNode"))
if err := xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil {
t.Log(i18n.GetMsgWithMap("PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()}))
return err
}
t.Log(i18n.GetMsgByKey("PushSSLToNodeSuccess"))
return nil
}, nil)
go func() {
if err := pushTask.Execute(); err != nil {
global.LOG.Errorf("push ssl to node failed, sslID: %d, err: %v", websiteSSL.ID, err)
}
}()
return nil
}
func (w WebsiteSSLService) DownloadFile(id uint) (*os.File, error) {
+1
View File
@@ -21,6 +21,7 @@ func (a *WebsiteSSLRouter) InitRouter(Router *gin.RouterGroup) {
groupRouter.GET("/website/:websiteId", baseApi.GetWebsiteSSLByWebsiteId)
groupRouter.GET("/:id", baseApi.GetWebsiteSSLById)
groupRouter.POST("/update", baseApi.UpdateWebsiteSSL)
groupRouter.POST("/push", baseApi.PushWebsiteSSLToNode)
groupRouter.POST("/upload", baseApi.UploadWebsiteSSL)
groupRouter.POST("/obtain", baseApi.ApplyWebsiteSSL)
groupRouter.POST("/download", baseApi.DownloadWebsiteSSL)
+14
View File
@@ -295,6 +295,15 @@ export namespace Website {
keyType: string;
pushDir: boolean;
dir: string;
pushNode?: boolean;
nodes?: string;
}
export interface SSLPush {
id: number;
pushNode: boolean;
nodes: string;
taskID: string;
}
export interface AcmeAccount extends CommonModel {
@@ -578,6 +587,9 @@ export namespace Website {
certificatePath: string;
type: string;
sslID: number;
description?: string;
pushNode?: boolean;
nodes?: string;
}
export interface SSLObtain {
@@ -620,6 +632,8 @@ export namespace Website {
pushDir: boolean;
dir: string;
description: string;
pushNode?: boolean;
nodes?: string;
}
export interface RenewSSLByCA {
+4
View File
@@ -148,6 +148,10 @@ export const updateSSL = (req: Website.SSLUpdate) => {
return http.post<any>(`/websites/ssl/update`, req);
};
export const pushSSLToNode = (req: Website.SSLPush) => {
return http.post<any>(`/websites/ssl/push`, req);
};
export const getDnsResolve = (req: Website.DNSResolveReq) => {
return http.post<Website.DNSResolve[]>(`/websites/ssl/resolve`, req, TimeoutEnum.T_5M);
};
@@ -62,6 +62,14 @@
{{ $t('ssl.shellHelper') }}
</span>
</el-form-item>
<PushToNode
v-if="isMaster && isXpackOrEE"
:push-node="obtain.pushNode"
:nodes="obtain.pushNodes"
type="ssl"
@update:push-node="obtain.pushNode = $event"
@update:nodes="obtain.pushNodes = $event"
/>
</el-form>
</el-col>
</el-row>
@@ -84,8 +92,20 @@ import i18n from '@/lang';
import FileList from '@/components/file-list/index.vue';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
import { defineAsyncComponent, ref } from 'vue';
import { KeyTypes } from '@/global/mimetype';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isMaster, isXpackOrEE } = useGlobalStore();
const PushToNode = defineAsyncComponent(async () => {
const modules = import.meta.glob('@/xpack/views/ssl/index.vue');
const loader = modules['/src/xpack/views/ssl/index.vue'];
if (loader) {
return ((await loader()) as any).default;
}
return { template: '<div></div>' };
});
const open = ref(false);
const fileRef = ref();
@@ -100,6 +120,7 @@ const rules = ref({
time: [Rules.integerNumber, checkNumberRange(1, 10000)],
shell: [Rules.requiredInput],
description: [checkMaxLength(128)],
pushNodes: [Rules.requiredSelect],
});
const initData = () => ({
@@ -114,6 +135,9 @@ const initData = () => ({
description: '',
execShell: false,
shell: '',
pushNode: false,
pushNodes: [] as string[],
nodes: '',
});
const obtain = ref(initData());
@@ -144,6 +168,7 @@ const submit = async (formEl: FormInstance | undefined) => {
return;
}
loading.value = true;
obtain.value.nodes = obtain.value.pushNode ? obtain.value.pushNodes.join(',') : '';
obtainSSLByCA(obtain.value)
.then(() => {
+128 -3
View File
@@ -182,13 +182,47 @@
<Log ref="logRef" @close="search()" :heightDiff="220" />
<CA ref="caRef" @close="search()" />
<Obtain ref="obtainRef" @close="search()" @submit="openLog" />
<DrawerPro v-model="pushOpen" :header="$t('commons.button.sync')" size="large" @close="handlePushClose">
<el-form
ref="pushFormRef"
label-position="top"
:model="pushForm"
:rules="pushRules"
v-loading="pushLoading"
>
<PushToNode
v-if="isMaster && isXpackOrEE"
:push-node="pushForm.pushNode"
:nodes="pushForm.pushNodes"
type="ssl"
@update:push-node="pushForm.pushNode = $event"
@update:nodes="pushForm.pushNodes = $event"
/>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handlePushClose" :disabled="pushLoading">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button
v-permission="'website_cert_manage'"
type="primary"
@click="submitPush"
:disabled="pushLoading"
>
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</DrawerPro>
<TaskLog ref="taskLogRef" @close="search()" />
</LayoutContent>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import { deleteSSL, downloadFile, searchSSL, updateSSL } from '@/api/modules/website';
import { defineAsyncComponent, onMounted, reactive, ref } from 'vue';
import { deleteSSL, downloadFile, pushSSLToNode, searchSSL, updateSSL } from '@/api/modules/website';
import DnsAccount from './dns-account/index.vue';
import AcmeAccount from './acme-account/index.vue';
import CA from './ca/index.vue';
@@ -206,9 +240,23 @@ import Obtain from './obtain/index.vue';
import MsgInfo from '@/components/msg-info/index.vue';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { useOperateNodeContext } from '@/composables/useOperateNodeContext';
import TaskLog from '@/components/log/task/index.vue';
import { newUUID } from '@/utils/id';
import { Rules } from '@/global/form-rules';
import { FormInstance } from 'element-plus';
const { currentNode, isMobile } = useGlobalStore();
const { currentNode, isMobile, isMaster, isXpackOrEE } = useGlobalStore();
useOperateNodeContext(currentNode);
const PushToNode = defineAsyncComponent(async () => {
const modules = import.meta.glob('@/xpack/views/ssl/index.vue');
const loader = modules['/src/xpack/views/ssl/index.vue'];
if (loader) {
return ((await loader()) as any).default;
}
return { template: '<div></div>' };
});
const paginationConfig = reactive({
cacheSizeKey: 'ssl-page-size',
currentPage: 1,
@@ -225,8 +273,12 @@ const opRef = ref();
const sslUploadRef = ref();
const applyRef = ref();
const logRef = ref();
const taskLogRef = ref();
const caRef = ref();
const obtainRef = ref();
const pushFormRef = ref<FormInstance>();
const pushOpen = ref(false);
const pushLoading = ref(false);
let selects = ref<any>([]);
const columns = ref([]);
const req = reactive({
@@ -234,6 +286,14 @@ const req = reactive({
orderBy: 'updated_at',
order: 'descending',
});
const pushForm = ref({
id: 0,
pushNode: true,
pushNodes: [] as string[],
});
const pushRules = ref({
pushNodes: [Rules.requiredSelect],
});
const routerButton = [
{
@@ -292,6 +352,19 @@ const buttons = [
return row.provider != 'manual';
},
},
{
label: i18n.global.t('commons.button.sync'),
permission: true,
disabled: function (row: Website.SSLDTO) {
return row.status !== 'ready';
},
click: function (row: Website.SSLDTO) {
openPush(row);
},
show: function (row: Website.SSLDTO) {
return isMaster.value && isXpackOrEE.value && row.provider !== 'fromMaster';
},
},
{
label: i18n.global.t('commons.button.download'),
click: function (row: Website.SSLDTO) {
@@ -396,6 +469,58 @@ const openSSLLog = (row: Website.SSL) => {
logRef.value.acceptParams({ id: row.id, type: 'ssl', tail: row.status === 'applying' });
};
const parsePushNodes = (nodes: string) => {
return nodes
? nodes
.split(',')
.map((item) => item.trim())
.filter((item) => item !== '')
: [];
};
const openPush = (row: Website.SSLDTO) => {
pushForm.value = {
id: row.id,
pushNode: true,
pushNodes: parsePushNodes(row.nodes),
};
pushOpen.value = true;
};
const handlePushClose = () => {
pushOpen.value = false;
pushFormRef.value?.resetFields();
pushForm.value = {
id: 0,
pushNode: true,
pushNodes: [],
};
};
const submitPush = async () => {
if (!pushForm.value.pushNode || pushForm.value.pushNodes.length === 0) {
MsgError(i18n.global.t('commons.rule.requiredSelect'));
return;
}
await pushFormRef.value?.validate();
const taskID = newUUID();
pushLoading.value = true;
pushSSLToNode({
id: pushForm.value.id,
pushNode: pushForm.value.pushNode,
nodes: pushForm.value.pushNodes.join(','),
taskID,
})
.then(() => {
handlePushClose();
taskLogRef.value.openWithTaskID(taskID);
search();
})
.finally(() => {
pushLoading.value = false;
});
};
const openCA = () => {
caRef.value.acceptParams();
};
@@ -69,6 +69,14 @@
<el-form-item :label="$t('website.remark')" prop="description">
<el-input v-model="ssl.description"></el-input>
</el-form-item>
<PushToNode
v-if="isMaster && isXpackOrEE"
:push-node="ssl.pushNode"
:nodes="ssl.pushNodes"
type="ssl"
@update:push-node="ssl.pushNode = $event"
@update:nodes="ssl.pushNodes = $event"
/>
</el-form>
<template #footer>
<span class="dialog-footer">
@@ -89,9 +97,21 @@ import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
import FileList from '@/components/file-list/index.vue';
import { ref } from 'vue';
import { defineAsyncComponent, ref } from 'vue';
import { MsgSuccess } from '@/utils/message';
import { Website } from '@/api/interface/website';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { isMaster, isXpackOrEE } = useGlobalStore();
const PushToNode = defineAsyncComponent(async () => {
const modules = import.meta.glob('@/xpack/views/ssl/index.vue');
const loader = modules['/src/xpack/views/ssl/index.vue'];
if (loader) {
return ((await loader()) as any).default;
}
return { template: '<div></div>' };
});
const open = ref(false);
const keyFileRef = ref();
@@ -111,6 +131,7 @@ const rules = ref({
type: [Rules.requiredSelect],
certificateFile: [Rules.requiredInput],
privateKeyFile: [Rules.requiredInput],
pushNodes: [Rules.requiredSelect],
});
const initData = () => ({
privateKey: '',
@@ -120,6 +141,9 @@ const initData = () => ({
type: 'paste',
sslID: 0,
description: '',
pushNode: false,
pushNodes: [] as string[],
nodes: '',
privateKeyFile: null as File | null,
certificateFile: null as File | null,
});
@@ -155,6 +179,13 @@ const acceptParams = (websiteSSL?: Website.SSLDTO) => {
ssl.value.description = websiteSSL.description;
ssl.value.privateKeyPath = websiteSSL.privateKeyPath;
ssl.value.certificatePath = websiteSSL.certPath;
ssl.value.pushNode = websiteSSL.pushNode;
ssl.value.pushNodes = websiteSSL.nodes
? websiteSSL.nodes
.split(',')
.map((item) => item.trim())
.filter((item) => item !== '')
: [];
if (ssl.value.certificatePath != '' && ssl.value.privateKeyPath != '') {
ssl.value.type = 'local';
}
@@ -174,11 +205,14 @@ const submit = async () => {
try {
await sslForm.value?.validate();
loading.value = true;
ssl.value.nodes = ssl.value.pushNode ? ssl.value.pushNodes.join(',') : '';
if (ssl.value.type === 'upload') {
const formData = new FormData();
formData.append('type', ssl.value.type);
formData.append('description', ssl.value.description);
formData.append('sslID', ssl.value.sslID.toString());
formData.append('pushNode', String(ssl.value.pushNode));
formData.append('nodes', ssl.value.nodes);
if (ssl.value.privateKeyFile) {
formData.append('privateKeyFile', ssl.value.privateKeyFile);