refactor: auto certificate options

1. Add OCSP Must Staple options #292
2. Add LEGO_DISABLE_CNAME_SUPPORT options #407
This commit is contained in:
Jacky 2024-07-24 22:53:22 +08:00
parent 532d6e83c5
commit 4660a46a7e
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
18 changed files with 234 additions and 212 deletions

View file

@ -117,14 +117,16 @@ func IssueCert(c *gin.Context) {
}
err = certModel.Updates(&model.Cert{
Domains: payload.ServerName,
SSLCertificatePath: payload.GetCertificatePath(),
SSLCertificateKeyPath: payload.GetCertificateKeyPath(),
AutoCert: model.AutoCertEnabled,
KeyType: payload.KeyType,
ChallengeMethod: payload.ChallengeMethod,
DnsCredentialID: payload.DNSCredentialID,
Resource: payload.Resource,
Domains: payload.ServerName,
SSLCertificatePath: payload.GetCertificatePath(),
SSLCertificateKeyPath: payload.GetCertificateKeyPath(),
AutoCert: model.AutoCertEnabled,
KeyType: payload.KeyType,
ChallengeMethod: payload.ChallengeMethod,
DnsCredentialID: payload.DNSCredentialID,
Resource: payload.Resource,
MustStaple: payload.MustStaple,
LegoDisableCNAMESupport: payload.LegoDisableCNAMESupport,
})
if err != nil {

View file

@ -5,21 +5,27 @@ export interface DNSProvider {
code?: string
provider?: string
configuration: {
credentials: {
[key: string]: string
}
additional: {
[key: string]: string
}
credentials: Record<string, string>
additional: Record<string, string>
}
links?: {
api: string
go_client: string
}
}
export interface DnsChallenge extends DNSProvider {
dns_credential_id: number | null
challenge_method: string
export interface AutoCertOptions {
name?: string
domains: string[]
code?: string
dns_credential_id?: number | null
challenge_method?: string
configuration?: DNSProvider['configuration']
key_type: string
acme_user_id?: number
provider?: string
must_staple?: boolean
lego_disable_cname_support?: boolean
}
const auto_cert = {

View file

@ -3,18 +3,19 @@ import type { SelectProps } from 'ant-design-vue'
import type { Ref } from 'vue'
import type { AcmeUser } from '@/api/acme_user'
import acme_user from '@/api/acme_user'
import type { Cert } from '@/api/cert'
import type { AutoCertOptions } from '@/api/auto_cert'
const users = ref([]) as Ref<AcmeUser[]>
// This data is provided by the Top StdCurd component,
// is the object that you are trying to modify it
// we externalize the dns_credential_id to the parent component,
// this is used to tell the backend which dns_credential to use
const data = inject('data') as Ref<Cert>
const data = defineModel<AutoCertOptions>('options', {
default: () => {
return {}
},
required: true,
})
const id = computed(() => {
return data.value.acme_user_id
return data.value?.acme_user_id
})
const user_idx = ref()
@ -35,7 +36,7 @@ watch(id, init)
watch(current, () => {
if (mounted.value)
data.value.acme_user_id = current.value.id
data.value!.acme_user_id = current.value.id
})
onMounted(async () => {
@ -84,8 +85,9 @@ const filterOption = (input: string, option: { label: string }) => {
<AFormItem :label="$gettext('ACME User')">
<ASelect
v-model:value="user_idx"
:placeholder="$gettext('System Initial User')"
show-search
:options="options"
:options
:filter-option="filterOption"
/>
</AFormItem>

View file

@ -52,12 +52,6 @@ function save() {
})
}
provide('data', data)
provide('no_server_name', computed(() => {
return false
}))
const log = computed(() => {
const logs = data.value.log?.split('\n')
@ -134,9 +128,17 @@ const isManaged = computed(() => {
</AForm>
<template v-if="isManaged">
<RenewCert @renewed="init" />
<RenewCert
:options="{
name: data.name,
domains: data.domains,
key_type: data.key_type,
}"
@renewed="init"
/>
<AutoCertStepOne
v-model:options="data"
style="max-width: 600px"
hide-note
/>

View file

@ -5,13 +5,6 @@ import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
import cert from '@/api/cert'
import WildcardCertificate from '@/views/certificate/WildcardCertificate.vue'
// DO NOT REMOVE THESE LINES
const no_server_name = computed(() => {
return false
})
provide('no_server_name', no_server_name)
const refWildcard = ref()
const refTable = ref()
</script>

View file

@ -1,8 +1,11 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import { message } from 'ant-design-vue'
import ObtainCertLive from '@/views/domain/cert/components/ObtainCertLive.vue'
import type { Cert } from '@/api/cert'
import type { AutoCertOptions } from '@/api/auto_cert'
const props = defineProps<{
options: AutoCertOptions
}>()
const emit = defineEmits<{
renewed: [void]
@ -13,12 +16,12 @@ const modalClosable = ref(true)
const refObtainCertLive = ref()
const data = inject('data') as Ref<Cert>
const issueCert = () => {
modalVisible.value = true
refObtainCertLive.value.issue_cert(data.value.name, data.value.domains, data.value.key_type).then(() => {
const { name, domains, key_type } = props.options
refObtainCertLive.value.issue_cert(name, domains, key_type).then(() => {
message.success($gettext('Renew successfully'))
emit('renewed')
})
@ -52,6 +55,7 @@ provide('issuing_cert', issuing_cert)
ref="refObtainCertLive"
v-model:modal-closable="modalClosable"
v-model:modal-visible="modalVisible"
:options
/>
</AModal>
</div>

View file

@ -1,10 +1,9 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import { message } from 'ant-design-vue'
import type { Cert } from '@/api/cert'
import ObtainCertLive from '@/views/domain/cert/components/ObtainCertLive.vue'
import DNSChallenge from '@/views/domain/cert/components/DNSChallenge.vue'
import { PrivateKeyTypeList } from '@/constants'
import type { AutoCertOptions } from '@/api/auto_cert'
import AutoCertStepOne from '@/views/domain/cert/components/AutoCertStepOne.vue'
const emit = defineEmits<{
issued: [void]
@ -12,10 +11,9 @@ const emit = defineEmits<{
const step = ref(0)
const visible = ref(false)
const data = ref({}) as Ref<Cert>
const data = ref({}) as Ref<AutoCertOptions>
const issuing_cert = ref(false)
provide('data', data)
provide('issuing_cert', issuing_cert)
function open() {
visible.value = true
@ -23,7 +21,7 @@ function open() {
data.value = {
challenge_method: 'dns01',
key_type: '2048',
} as Cert
} as AutoCertOptions
}
defineExpose({
@ -66,8 +64,6 @@ const issueCert = () => {
force-render
>
<template v-if="step === 0">
<DNSChallenge />
<AForm layout="vertical">
<AFormItem :label="$gettext('Domain')">
<AInput
@ -75,19 +71,15 @@ const issueCert = () => {
addon-before="*."
/>
</AFormItem>
<AFormItem :label="$gettext('Key Type')">
<ASelect v-model:value="data.key_type">
<ASelectOption
v-for="t in PrivateKeyTypeList"
:key="t.key"
:value="t.key"
>
{{ t.name }}
</ASelectOption>
</ASelect>
</AFormItem>
</AForm>
<AutoCertStepOne
v-model:options="data"
style="max-width: 600px"
hide-note
force-dns-challenge
/>
<div
v-if="step === 0"
class="flex justify-end"
@ -106,6 +98,7 @@ const issueCert = () => {
ref="refObtainCertLive"
v-model:modal-closable="modalClosable"
v-model:modal-visible="modalVisible"
:options="data"
/>
</AModal>
</div>

View file

@ -2,11 +2,9 @@
import ObtainCert from '@/views/domain/cert/components/ObtainCert.vue'
import type { NgxDirective } from '@/api/ngx'
export interface Props {
defineProps<{
configName: string
}
const props = defineProps<Props>()
}>()
const issuing_cert = ref(false)
const obtain_cert = ref()
@ -16,18 +14,16 @@ const enabled = defineModel<boolean>('enabled', {
default: () => false,
})
const no_server_name = computed(() => {
const noServerName = computed(() => {
if (!directivesMap.value.server_name)
return true
return directivesMap.value.server_name.length === 0
})
provide('no_server_name', no_server_name)
provide('props', props)
provide('issuing_cert', issuing_cert)
watch(no_server_name, () => {
watch(noServerName, () => {
enabled.value = false
})
@ -45,6 +41,8 @@ async function onchange() {
<ObtainCert
ref="obtain_cert"
:key="update"
:no-server-name="noServerName"
:config-name="configName"
@update:auto_cert="r => enabled = r"
/>
<div class="issue-cert">
@ -52,7 +50,7 @@ async function onchange() {
<ASwitch
:loading="issuing_cert"
:checked="enabled"
:disabled="no_server_name"
:disabled="noServerName"
@change="onchange"
/>
</AFormItem>

View file

@ -1,43 +1,38 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import type { DnsChallenge } from '@/api/auto_cert'
import { $gettext } from '../../../../gettext'
import type { AutoCertOptions } from '@/api/auto_cert'
import DNSChallenge from '@/views/domain/cert/components/DNSChallenge.vue'
import type { Cert } from '@/api/cert'
import ACMEUserSelector from '@/views/certificate/ACMEUserSelector.vue'
import { PrivateKeyTypeList } from '@/constants'
defineProps<{
const props = defineProps<{
hideNote?: boolean
forceDnsChallenge?: boolean
}>()
const no_server_name = inject('no_server_name')
// Provide by ObtainCert.vue
const data = inject('data') as Ref<DnsChallenge & Cert>
const data = defineModel<AutoCertOptions>('options', {
default: () => {
return {}
},
required: true,
})
onMounted(() => {
if (!data.value.key_type)
data.value.key_type = '2048'
if (props.forceDnsChallenge)
data.value.challenge_method = 'dns01'
})
watch(() => props.forceDnsChallenge, v => {
if (v)
data.value.challenge_method = 'dns01'
})
</script>
<template>
<div>
<template v-if="no_server_name">
<AAlert
:message="$gettext('Warning')"
type="warning"
show-icon
>
<template #description>
<span v-if="no_server_name">
{{ $gettext('server_name parameter is required') }}
</span>
</template>
</AAlert>
<br>
</template>
<AAlert
v-if="!hideNote"
type="info"
@ -52,8 +47,8 @@ onMounted(() => {
+ 'multiple domains.') }}
</p>
<p>
{{ $gettext('The certificate for the domain will be checked 5 minutes, '
+ 'and will be renewed if it has been more than 1 week since it was last issued.') }}
{{ $gettext('The certificate for the domain will be checked 30 minutes, '
+ 'and will be renewed if it has been more than 1 week or the period you set in settings since it was last issued.') }}
</p>
<p v-if="data.challenge_method === 'http01'">
{{ $gettext('Make sure you have configured a reverse proxy for .well-known '
@ -67,7 +62,10 @@ onMounted(() => {
</template>
</AAlert>
<AForm layout="vertical">
<AFormItem :label="$gettext('Challenge Method')">
<AFormItem
v-if="!forceDnsChallenge"
:label="$gettext('Challenge Method')"
>
<ASelect v-model:value="data.challenge_method">
<ASelectOption value="http01">
{{ $gettext('HTTP01') }}
@ -89,8 +87,32 @@ onMounted(() => {
</ASelect>
</AFormItem>
</AForm>
<ACMEUserSelector />
<DNSChallenge v-if="data.challenge_method === 'dns01'" />
<ACMEUserSelector v-model:options="data" />
<DNSChallenge
v-if="data.challenge_method === 'dns01'"
v-model:options="data"
/>
<AForm layout="vertical">
<AFormItem :label="$gettext('OCSP Must Staple')">
<template #help>
<p>
{{ $gettext('Do not enable this option unless you are sure that you need it.') }}
{{ $gettext('OCSP Must Staple may cause errors for some users on first access using Firefox.') }}
<a href="https://github.com/0xJacky/nginx-ui/issues/322">#322</a>
</p>
</template>
<ASwitch v-model:checked="data.must_staple" />
</AFormItem>
<AFormItem :label="$gettext('Lego disable CNAME Support')">
<template #help>
<p>
{{ $gettext('If your domain has CNAME records and you cannot obtain certificates, '
+ 'you need to enable this option.') }}
</p>
</template>
<ASwitch v-model:checked="data.lego_disable_cname_support" />
</AFormItem>
</AForm>
</div>
</template>

View file

@ -1,19 +1,19 @@
<script setup lang="ts">
import type { SelectProps } from 'ant-design-vue'
import type { Ref } from 'vue'
import type { SelectValue } from 'ant-design-vue/es/select'
import type { DNSProvider } from '@/api/auto_cert'
import type { AutoCertOptions, DNSProvider } from '@/api/auto_cert'
import auto_cert from '@/api/auto_cert'
import dns_credential from '@/api/dns_credential'
const providers = ref([]) as Ref<DNSProvider[]>
const credentials = ref<SelectProps['options']>([])
// This data is provided by the Top StdCurd component,
// is the object that you are trying to modify it
// we externalize the dns_credential_id to the parent component,
// this is used to tell the backend which dns_credential to use
const data = inject('data') as Ref<DNSProvider & { dns_credential_id: SelectValue }>
const data = defineModel<AutoCertOptions>('options', {
default: () => {
return {}
},
required: true,
})
const code = computed(() => {
return data.value.code
@ -95,7 +95,7 @@ const filterOption = (input: string, option: { label: string }) => {
<ASelect
v-model:value="provider_idx"
show-search
:options="options"
:options
:filter-option="filterOption"
/>
</AFormItem>
@ -105,7 +105,7 @@ const filterOption = (input: string, option: { label: string }) => {
:rules="[{ required: true }]"
>
<ASelect
v-model:value="data.dns_credential_id"
v-model:value="data.dns_credential_id as any"
:options="credentials"
/>
</AFormItem>

View file

@ -4,12 +4,16 @@ import type { ComputedRef, Ref } from 'vue'
import domain from '@/api/domain'
import AutoCertStepOne from '@/views/domain/cert/components/AutoCertStepOne.vue'
import type { NgxConfig, NgxDirective } from '@/api/ngx'
import type { Props } from '@/views/domain/cert/IssueCert.vue'
import type { DnsChallenge } from '@/api/auto_cert'
import type { AutoCertOptions } from '@/api/auto_cert'
import ObtainCertLive from '@/views/domain/cert/components/ObtainCertLive.vue'
import type { CertificateResult } from '@/api/cert'
import type { PrivateKeyType } from '@/constants'
const props = defineProps<{
configName: string
noServerName?: boolean
}>()
const emit = defineEmits(['update:auto_cert'])
const modalVisible = ref(false)
@ -26,15 +30,11 @@ const data = ref({
credentials: {},
additional: {},
},
}) as Ref<DnsChallenge>
}) as Ref<AutoCertOptions>
const modalClosable = ref(true)
provide('data', data)
const save_config = inject('save_config') as () => Promise<void>
const no_server_name = inject('no_server_name') as Ref<boolean>
const props = inject('props') as Props
const issuing_cert = inject('issuing_cert') as Ref<boolean>
const ngx_config = inject('ngx_config') as NgxConfig
const current_server_directives = inject('current_server_directives') as ComputedRef<NgxDirective[]>
@ -61,8 +61,8 @@ function change_auto_cert(status: boolean, key_type?: PrivateKeyType) {
if (status) {
domain.add_auto_cert(props.configName, {
domains: name.value.trim().split(' '),
challenge_method: data.value.challenge_method,
dns_credential_id: data.value.dns_credential_id,
challenge_method: data.value.challenge_method!,
dns_credential_id: data.value.dns_credential_id!,
key_type: key_type!,
}).then(() => {
message.success($gettext('Auto-renewal enabled for %{name}', { name: name.value }))
@ -98,7 +98,7 @@ function job() {
modalClosable.value = false
issuing_cert.value = true
if (no_server_name.value) {
if (props.noServerName) {
message.error($gettext('server_name not found in directives'))
issuing_cert.value = false
@ -183,13 +183,17 @@ function next() {
force-render
>
<template v-if="step === 1">
<AutoCertStepOne />
<AutoCertStepOne
v-model:options="data"
:no-server-name="noServerName"
/>
</template>
<template v-else-if="step === 2">
<ObtainCertLive
ref="refObtainCertLive"
v-model:modal-closable="modalClosable"
v-model:modal-visible="modalVisible"
:options="data"
/>
</template>
<div

View file

@ -1,40 +1,17 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import websocket from '@/lib/websocket'
import type { DnsChallenge } from '@/api/auto_cert'
import Error from '@/views/other/Error.vue'
import type { CertificateResult } from '@/api/cert'
import type { AutoCertOptions } from '@/api/auto_cert'
const props = defineProps<{
modalClosable: boolean
modalVisible: boolean
options: AutoCertOptions
}>()
const emit = defineEmits<{
'update:modalClosable': [value: boolean]
'update:modalVisible': [value: boolean]
}>()
const modalClosable = computed({
get() {
return props.modalClosable
},
set(value) {
emit('update:modalClosable', value)
},
})
const modalVisible = computed({
get() {
return props.modalVisible
},
set(value) {
emit('update:modalVisible', value)
},
})
const modalVisible = defineModel<boolean>('modalVisible')
const modalClosable = defineModel<boolean>('modalClosable')
const issuing_cert = inject('issuing_cert') as Ref<boolean>
const data = inject('data') as Ref<DnsChallenge>
const progressStrokeColor = {
from: '#108ee9',
@ -71,8 +48,8 @@ const issue_cert = async (config_name: string, server_name: string[], key_type:
ws.onopen = () => {
ws.send(JSON.stringify({
server_name,
...props.options,
key_type,
...data.value,
}))
}
@ -114,7 +91,7 @@ const issue_cert = async (config_name: string, server_name: string[], key_type:
}
else {
progressStatus.value = 'exception'
reject(new Error($gettext('Fail to obtain certificate')))
reject($gettext('Fail to obtain certificate'))
}
break
}

View file

@ -71,12 +71,14 @@ func autoCert(certModel *model.Cert) {
// support SAN certification
payload := &ConfigPayload{
CertID: certModel.ID,
ServerName: certModel.Domains,
ChallengeMethod: certModel.ChallengeMethod,
DNSCredentialID: certModel.DnsCredentialID,
KeyType: certModel.GetKeyType(),
NotBefore: certInfo.NotBefore,
CertID: certModel.ID,
ServerName: certModel.Domains,
ChallengeMethod: certModel.ChallengeMethod,
DNSCredentialID: certModel.DnsCredentialID,
KeyType: certModel.GetKeyType(),
NotBefore: certInfo.NotBefore,
MustStaple: certModel.MustStaple,
LegoDisableCNAMESupport: certModel.LegoDisableCNAMESupport,
}
if certModel.Resource != nil {

View file

@ -130,7 +130,6 @@ func IssueCert(payload *ConfigPayload, logChan chan string, errChan chan error)
errChan <- errors.Wrap(err, "environment configuration is empty")
return
}
}
if err != nil {
@ -138,6 +137,18 @@ func IssueCert(payload *ConfigPayload, logChan chan string, errChan chan error)
return
}
// fix #407
if payload.LegoDisableCNAMESupport {
err = os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", "true")
if err != nil {
errChan <- errors.Wrap(err, "set env flag to disable lego CNAME support error")
return
}
defer func() {
_ = os.Unsetenv("LEGO_DISABLE_CNAME_SUPPORT")
}()
}
if time.Now().Sub(payload.NotBefore).Hours()/24 <= 21 &&
payload.Resource != nil && payload.Resource.Certificate != nil {
renew(payload, client, l, errChan)

View file

@ -10,8 +10,9 @@ import (
func obtain(payload *ConfigPayload, client *lego.Client, l *log.Logger, errChan chan error) {
request := certificate.ObtainRequest{
Domains: payload.ServerName,
Bundle: true,
Domains: payload.ServerName,
Bundle: true,
MustStaple: payload.MustStaple,
}
l.Println("[INFO] [Nginx UI] Obtaining certificate")

View file

@ -16,17 +16,19 @@ import (
)
type ConfigPayload struct {
CertID int `json:"cert_id"`
ServerName []string `json:"server_name"`
ChallengeMethod string `json:"challenge_method"`
DNSCredentialID int `json:"dns_credential_id"`
ACMEUserID int `json:"acme_user_id"`
KeyType certcrypto.KeyType `json:"key_type"`
Resource *model.CertificateResource `json:"resource,omitempty"`
NotBefore time.Time `json:"-"`
CertificateDir string `json:"-"`
SSLCertificatePath string `json:"-"`
SSLCertificateKeyPath string `json:"-"`
CertID int `json:"cert_id"`
ServerName []string `json:"server_name"`
ChallengeMethod string `json:"challenge_method"`
DNSCredentialID int `json:"dns_credential_id"`
ACMEUserID int `json:"acme_user_id"`
KeyType certcrypto.KeyType `json:"key_type"`
Resource *model.CertificateResource `json:"resource,omitempty"`
MustStaple bool `json:"must_staple"`
LegoDisableCNAMESupport bool `json:"lego_disable_cname_support"`
NotBefore time.Time `json:"-"`
CertificateDir string `json:"-"`
SSLCertificatePath string `json:"-"`
SSLCertificateKeyPath string `json:"-"`
}
func (c *ConfigPayload) GetACMEUser() (user *model.AcmeUser, err error) {

View file

@ -1,38 +1,39 @@
package cert
import (
"github.com/0xJacky/Nginx-UI/model"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/pkg/errors"
"log"
"github.com/0xJacky/Nginx-UI/model"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/pkg/errors"
"log"
)
func renew(payload *ConfigPayload, client *lego.Client, l *log.Logger, errChan chan error) {
if payload.Resource == nil {
errChan <- errors.New("resource is nil")
return
}
if payload.Resource == nil {
errChan <- errors.New("resource is nil")
return
}
options := &certificate.RenewOptions{
Bundle: true,
}
options := &certificate.RenewOptions{
Bundle: true,
MustStaple: payload.MustStaple,
}
cert, err := client.Certificate.RenewWithOptions(payload.Resource.GetResource(), options)
if err != nil {
errChan <- errors.Wrap(err, "renew cert error")
return
}
cert, err := client.Certificate.RenewWithOptions(payload.Resource.GetResource(), options)
if err != nil {
errChan <- errors.Wrap(err, "renew cert error")
return
}
payload.Resource = &model.CertificateResource{
Resource: cert,
PrivateKey: cert.PrivateKey,
Certificate: cert.Certificate,
IssuerCertificate: cert.IssuerCertificate,
CSR: cert.CSR,
}
payload.Resource = &model.CertificateResource{
Resource: cert,
PrivateKey: cert.PrivateKey,
Certificate: cert.Certificate,
IssuerCertificate: cert.IssuerCertificate,
CSR: cert.CSR,
}
payload.WriteFile(l, errChan)
payload.WriteFile(l, errChan)
l.Println("[INFO] [Nginx UI] Certificate renewed successfully")
l.Println("[INFO] [Nginx UI] Certificate renewed successfully")
}

View file

@ -30,21 +30,23 @@ type CertificateResource struct {
type Cert struct {
Model
Name string `json:"name"`
Domains pq.StringArray `json:"domains" gorm:"type:text[]"`
Filename string `json:"filename"`
SSLCertificatePath string `json:"ssl_certificate_path"`
SSLCertificateKeyPath string `json:"ssl_certificate_key_path"`
AutoCert int `json:"auto_cert"`
ChallengeMethod string `json:"challenge_method"`
DnsCredentialID int `json:"dns_credential_id"`
DnsCredential *DnsCredential `json:"dns_credential,omitempty"`
ACMEUserID int `json:"acme_user_id"`
ACMEUser *AcmeUser `json:"acme_user,omitempty"`
KeyType certcrypto.KeyType `json:"key_type"`
Log string `json:"log"`
Resource *CertificateResource `json:"-" gorm:"serializer:json"`
SyncNodeIds []int `json:"sync_node_ids" gorm:"serializer:json"`
Name string `json:"name"`
Domains pq.StringArray `json:"domains" gorm:"type:text[]"`
Filename string `json:"filename"`
SSLCertificatePath string `json:"ssl_certificate_path"`
SSLCertificateKeyPath string `json:"ssl_certificate_key_path"`
AutoCert int `json:"auto_cert"`
ChallengeMethod string `json:"challenge_method"`
DnsCredentialID int `json:"dns_credential_id"`
DnsCredential *DnsCredential `json:"dns_credential,omitempty"`
ACMEUserID int `json:"acme_user_id"`
ACMEUser *AcmeUser `json:"acme_user,omitempty"`
KeyType certcrypto.KeyType `json:"key_type"`
Log string `json:"log"`
Resource *CertificateResource `json:"-" gorm:"serializer:json"`
SyncNodeIds []int `json:"sync_node_ids" gorm:"serializer:json"`
MustStaple bool `json:"must_staple"`
LegoDisableCNAMESupport bool `json:"lego_disable_cname_support"`
}
func FirstCert(confName string) (c Cert, err error) {