mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-15 19:17:02 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
522 lines
17 KiB
Go
522 lines
17 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package pki
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
type acmeContext struct {
|
|
// baseUrl is the combination of the configured cluster local URL and the acmePath up to /acme/
|
|
baseUrl *url.URL
|
|
clusterUrl *url.URL
|
|
sc *storageContext
|
|
role *roleEntry
|
|
issuer *issuerEntry
|
|
// acmeDirectory is a string that can distinguish the various acme directories we have configured
|
|
// if something needs to remain locked into a directory path structure.
|
|
acmeDirectory string
|
|
eabPolicy EabPolicy
|
|
ciepsPolicy string
|
|
runtimeOpts acmeWrapperOpts
|
|
}
|
|
|
|
func (c acmeContext) getAcmeState() *acmeState {
|
|
return c.sc.Backend.acmeState
|
|
}
|
|
|
|
type (
|
|
acmeOperation func(acmeCtx *acmeContext, r *logical.Request, _ *framework.FieldData) (*logical.Response, error)
|
|
acmeParsedOperation func(acmeCtx *acmeContext, r *logical.Request, fields *framework.FieldData, userCtx *jwsCtx, data map[string]interface{}) (*logical.Response, error)
|
|
acmeAccountRequiredOperation func(acmeCtx *acmeContext, r *logical.Request, fields *framework.FieldData, userCtx *jwsCtx, data map[string]interface{}, acct *acmeAccount) (*logical.Response, error)
|
|
)
|
|
|
|
// setupAcmeDirectory will populate a prefix'd URL with all the paths required
|
|
// for a given ACME directory.
|
|
func setupAcmeDirectory(b *backend, acmePrefix string, unauthPrefix string, opts acmeWrapperOpts) {
|
|
acmePrefix = strings.TrimRight(acmePrefix, "/")
|
|
unauthPrefix = strings.TrimRight(unauthPrefix, "/")
|
|
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeDirectory(b, acmePrefix, opts))
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeNonce(b, acmePrefix, opts))
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeNewAccount(b, acmePrefix, opts))
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeUpdateAccount(b, acmePrefix, opts))
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeGetOrder(b, acmePrefix, opts))
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeListOrders(b, acmePrefix, opts))
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeNewOrder(b, acmePrefix, opts))
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeFinalizeOrder(b, acmePrefix, opts))
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeFetchOrderCert(b, acmePrefix, opts))
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeChallenge(b, acmePrefix, opts))
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeAuthorization(b, acmePrefix, opts))
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeRevoke(b, acmePrefix, opts))
|
|
b.Backend.Paths = append(b.Backend.Paths, pathAcmeNewEab(b, acmePrefix)) // auth'd API that lives underneath the various /acme paths
|
|
|
|
// Add specific un-auth'd paths for ACME APIs
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/directory")
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/new-nonce")
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/new-account")
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/new-order")
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/revoke-cert")
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/key-change")
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/account/+")
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/authorization/+")
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/challenge/+/+")
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/orders")
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/order/+")
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/order/+/finalize")
|
|
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, unauthPrefix+"/order/+/cert")
|
|
// We specifically do NOT add acme/new-eab to this as it should be auth'd
|
|
}
|
|
|
|
// acmeErrorWrapper the lowest level wrapper that will translate errors into proper ACME error responses
|
|
func acmeErrorWrapper(op framework.OperationFunc) framework.OperationFunc {
|
|
return func(ctx context.Context, r *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
resp, err := op(ctx, r, data)
|
|
if err != nil {
|
|
return TranslateError(err)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
}
|
|
|
|
type acmeWrapperOpts struct {
|
|
isDefault bool
|
|
isCiepsEnabled bool
|
|
}
|
|
|
|
func (o acmeWrapperOpts) Clone() acmeWrapperOpts {
|
|
return acmeWrapperOpts{
|
|
isDefault: o.isDefault,
|
|
isCiepsEnabled: o.isCiepsEnabled,
|
|
}
|
|
}
|
|
|
|
// acmeWrapper a basic wrapper that all ACME handlers should leverage as the basis.
|
|
// This will create a basic ACME context, validate basic ACME configuration is setup
|
|
// for operations. This pulls in acmeErrorWrapper to translate error messages for users,
|
|
// but does not enforce any sort of ACME authentication.
|
|
func (b *backend) acmeWrapper(opts acmeWrapperOpts, op acmeOperation) framework.OperationFunc {
|
|
return acmeErrorWrapper(func(ctx context.Context, r *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
|
sc := b.makeStorageContext(ctx, r.Storage)
|
|
|
|
config, err := sc.Backend.acmeState.getConfigWithUpdate(sc)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch ACME configuration: %w", err)
|
|
}
|
|
|
|
// use string form in case someone messes up our config from raw storage.
|
|
eabPolicy, err := getEabPolicyByString(string(config.EabPolicyName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if isAcmeDisabled(sc, config, eabPolicy) {
|
|
return nil, ErrAcmeDisabled
|
|
}
|
|
|
|
if b.useLegacyBundleCaStorage() {
|
|
return nil, fmt.Errorf("%w: Can not perform ACME operations until migration has completed", ErrServerInternal)
|
|
}
|
|
|
|
acmeBaseUrl, clusterBase, err := getAcmeBaseUrl(sc, r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
role, issuer, err := getAcmeRoleAndIssuer(sc, data, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
acmeDirectory, err := getAcmeDirectory(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isCiepsEnabled, ciepsPolicy, err := getCiepsAcmeSettings(sc, opts, config, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
runtimeOpts := opts.Clone()
|
|
|
|
if isCiepsEnabled {
|
|
// We need to possibly reset the isCiepsEnabled option to true if we are in
|
|
// the default folder with the external-policy set as it would have been
|
|
// normally disabled.
|
|
if runtimeOpts.isDefault {
|
|
runtimeOpts.isCiepsEnabled = true
|
|
}
|
|
}
|
|
|
|
acmeCtx := &acmeContext{
|
|
baseUrl: acmeBaseUrl,
|
|
clusterUrl: clusterBase,
|
|
sc: sc,
|
|
role: role,
|
|
issuer: issuer,
|
|
acmeDirectory: acmeDirectory,
|
|
eabPolicy: eabPolicy,
|
|
ciepsPolicy: ciepsPolicy,
|
|
runtimeOpts: runtimeOpts,
|
|
}
|
|
|
|
return op(acmeCtx, r, data)
|
|
})
|
|
}
|
|
|
|
// acmeParsedWrapper is an ACME wrapper that will parse out the ACME request parameters, validate
|
|
// that we have a proper signature and pass to the operation a decoded map of arguments received.
|
|
// This wrapper builds on top of acmeWrapper. Note that this does perform signature verification
|
|
// it does not enforce the account being in a valid state nor existing.
|
|
func (b *backend) acmeParsedWrapper(opt acmeWrapperOpts, op acmeParsedOperation) framework.OperationFunc {
|
|
return b.acmeWrapper(opt, func(acmeCtx *acmeContext, r *logical.Request, fields *framework.FieldData) (*logical.Response, error) {
|
|
user, data, err := b.acmeState.ParseRequestParams(acmeCtx, r, fields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := op(acmeCtx, r, fields, user, data)
|
|
|
|
// Our response handlers might not add the necessary headers.
|
|
if resp != nil {
|
|
if resp.Headers == nil {
|
|
resp.Headers = map[string][]string{}
|
|
}
|
|
|
|
if _, ok := resp.Headers["Replay-Nonce"]; !ok {
|
|
nonce, _, err := b.acmeState.GetNonce()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp.Headers["Replay-Nonce"] = []string{nonce}
|
|
}
|
|
|
|
if _, ok := resp.Headers["Link"]; !ok {
|
|
resp.Headers["Link"] = genAcmeLinkHeader(acmeCtx)
|
|
} else {
|
|
directory := genAcmeLinkHeader(acmeCtx)[0]
|
|
addDirectory := true
|
|
for _, item := range resp.Headers["Link"] {
|
|
if item == directory {
|
|
addDirectory = false
|
|
break
|
|
}
|
|
}
|
|
if addDirectory {
|
|
resp.Headers["Link"] = append(resp.Headers["Link"], directory)
|
|
}
|
|
}
|
|
|
|
// ACME responses don't understand Vault's default encoding
|
|
// format. Rather than expecting everything to handle creating
|
|
// ACME-formatted responses, do the marshaling in one place.
|
|
if _, ok := resp.Data[logical.HTTPRawBody]; !ok {
|
|
ignored_values := map[string]bool{logical.HTTPContentType: true, logical.HTTPStatusCode: true}
|
|
fields := map[string]interface{}{}
|
|
body := map[string]interface{}{
|
|
logical.HTTPContentType: "application/json",
|
|
logical.HTTPStatusCode: http.StatusOK,
|
|
}
|
|
|
|
for key, value := range resp.Data {
|
|
if _, present := ignored_values[key]; !present {
|
|
fields[key] = value
|
|
} else {
|
|
body[key] = value
|
|
}
|
|
}
|
|
|
|
rawBody, err := json.Marshal(fields)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error marshaling JSON body: %w", err)
|
|
}
|
|
|
|
body[logical.HTTPRawBody] = rawBody
|
|
resp.Data = body
|
|
}
|
|
}
|
|
|
|
return resp, err
|
|
})
|
|
}
|
|
|
|
// acmeAccountRequiredWrapper builds on top of acmeParsedWrapper, enforcing the
|
|
// request has a proper signature for an existing account, and that account is
|
|
// in a valid status. It passes to the operation a decoded form of the request
|
|
// parameters as well as the ACME account the request is for.
|
|
func (b *backend) acmeAccountRequiredWrapper(opt acmeWrapperOpts, op acmeAccountRequiredOperation) framework.
|
|
OperationFunc {
|
|
return b.acmeParsedWrapper(opt, func(acmeCtx *acmeContext, r *logical.Request, fields *framework.FieldData, uc *jwsCtx, data map[string]interface{}) (*logical.Response, error) {
|
|
if !uc.Existing {
|
|
return nil, fmt.Errorf("cannot process request without a 'kid': %w", ErrMalformed)
|
|
}
|
|
|
|
account, err := requireValidAcmeAccount(acmeCtx, uc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return op(acmeCtx, r, fields, uc, data, account)
|
|
})
|
|
}
|
|
|
|
func requireValidAcmeAccount(acmeCtx *acmeContext, uc *jwsCtx) (*acmeAccount, error) {
|
|
account, err := acmeCtx.getAcmeState().LoadAccount(acmeCtx, uc.Kid)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error loading account: %w", err)
|
|
}
|
|
|
|
if err = acmeCtx.eabPolicy.EnforceForExistingAccount(account); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if account.Status != AccountStatusValid {
|
|
// Treating "revoked" and "deactivated" as the same here.
|
|
return nil, fmt.Errorf("%w: account in status: %s", ErrUnauthorized, account.Status)
|
|
}
|
|
return account, nil
|
|
}
|
|
|
|
func getAcmeBaseUrl(sc *storageContext, r *logical.Request) (*url.URL, *url.URL, error) {
|
|
baseUrl, err := getBasePathFromClusterConfig(sc)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
directoryPrefix, err := getAcmeDirectory(r)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return baseUrl.JoinPath(directoryPrefix), baseUrl, nil
|
|
}
|
|
|
|
func getBasePathFromClusterConfig(sc *storageContext) (*url.URL, error) {
|
|
cfg, err := sc.getClusterConfig()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed loading cluster config: %w", err)
|
|
}
|
|
|
|
if cfg.Path == "" {
|
|
return nil, fmt.Errorf("ACME feature requires local cluster 'path' field configuration to be set")
|
|
}
|
|
|
|
baseUrl, err := url.Parse(cfg.Path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed parsing URL configured in local cluster 'path' configuration: %s: %s",
|
|
cfg.Path, err.Error())
|
|
}
|
|
return baseUrl, nil
|
|
}
|
|
|
|
func getAcmeIssuer(sc *storageContext, issuerName string) (*issuerEntry, error) {
|
|
if issuerName == "" {
|
|
issuerName = defaultRef
|
|
}
|
|
issuerId, err := sc.resolveIssuerReference(issuerName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: issuer does not exist", ErrMalformed)
|
|
}
|
|
|
|
issuer, err := sc.fetchIssuerById(issuerId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("issuer failed to load: %w", err)
|
|
}
|
|
|
|
if issuer.Usage.HasUsage(IssuanceUsage) && len(issuer.KeyID) > 0 {
|
|
return issuer, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("%w: issuer missing proper issuance usage or key", ErrServerInternal)
|
|
}
|
|
|
|
// getAcmeDirectory return the base acme directory path, without a leading '/' and including
|
|
// the trailing /acme/ folder which is the root of all our various directories
|
|
func getAcmeDirectory(r *logical.Request) (string, error) {
|
|
acmePath := r.Path
|
|
if !strings.HasPrefix(acmePath, "/") {
|
|
acmePath = "/" + acmePath
|
|
}
|
|
|
|
lastIndex := strings.LastIndex(acmePath, "/acme/")
|
|
if lastIndex == -1 {
|
|
return "", fmt.Errorf("%w: unable to determine acme base folder path: %s", ErrServerInternal, acmePath)
|
|
}
|
|
|
|
// Skip the leading '/' and return our base path with the /acme/
|
|
return strings.TrimLeft(acmePath[0:lastIndex]+"/acme/", "/"), nil
|
|
}
|
|
|
|
func getAcmeRoleAndIssuer(sc *storageContext, data *framework.FieldData, config *acmeConfigEntry) (*roleEntry, *issuerEntry, error) {
|
|
requestedIssuer := getRequestedAcmeIssuerFromPath(data)
|
|
requestedRole := getRequestedAcmeRoleFromPath(data)
|
|
issuerToLoad := requestedIssuer
|
|
|
|
var role *roleEntry
|
|
var err error
|
|
|
|
if len(requestedRole) == 0 { // Default Directory
|
|
policyType, extraInfo, err := getDefaultDirectoryPolicyType(config.DefaultDirectoryPolicy)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
switch policyType {
|
|
case Forbid:
|
|
return nil, nil, fmt.Errorf("%w: default directory not allowed by ACME policy", ErrServerInternal)
|
|
case SignVerbatim, ExternalPolicy:
|
|
role = buildSignVerbatimRoleWithNoData(&roleEntry{
|
|
Issuer: requestedIssuer,
|
|
NoStore: false,
|
|
Name: requestedRole,
|
|
})
|
|
case Role:
|
|
role, err = getAndValidateAcmeRole(sc, extraInfo)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
} else { // Requested Role
|
|
role, err = getAndValidateAcmeRole(sc, requestedRole)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Check the Requested Role is Allowed
|
|
allowAnyRole := len(config.AllowedRoles) == 1 && config.AllowedRoles[0] == "*"
|
|
if !allowAnyRole {
|
|
|
|
var foundRole bool
|
|
for _, name := range config.AllowedRoles {
|
|
if name == role.Name {
|
|
foundRole = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !foundRole {
|
|
return nil, nil, fmt.Errorf("%w: specified role not allowed by ACME policy", ErrServerInternal)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we haven't loaded an issuer directly from our path and the specified (or default)
|
|
// role does specify an issuer prefer the role's issuer rather than the default issuer.
|
|
if len(role.Issuer) > 0 && len(requestedIssuer) == 0 {
|
|
issuerToLoad = role.Issuer
|
|
}
|
|
|
|
issuer, err := getAcmeIssuer(sc, issuerToLoad)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
allowAnyIssuer := len(config.AllowedIssuers) == 1 && config.AllowedIssuers[0] == "*"
|
|
if !allowAnyIssuer {
|
|
var foundIssuer bool
|
|
for index, name := range config.AllowedIssuers {
|
|
candidateId, err := sc.resolveIssuerReference(name)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to resolve reference for allowed_issuer entry %d: %w", index, err)
|
|
}
|
|
|
|
if candidateId == issuer.ID {
|
|
foundIssuer = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !foundIssuer {
|
|
return nil, nil, fmt.Errorf("%w: specified issuer not allowed by ACME policy", ErrServerInternal)
|
|
}
|
|
}
|
|
|
|
// If not allowed in configuration, override ExtKeyUsage behavior to force it to only be
|
|
// ServerAuth within ACME issued certs
|
|
if !config.AllowRoleExtKeyUsage {
|
|
role.ExtKeyUsage = []string{"serverauth"}
|
|
role.ExtKeyUsageOIDs = []string{}
|
|
role.ServerFlag = true
|
|
role.ClientFlag = false
|
|
role.CodeSigningFlag = false
|
|
role.EmailProtectionFlag = false
|
|
}
|
|
|
|
return role, issuer, nil
|
|
}
|
|
|
|
func getAndValidateAcmeRole(sc *storageContext, requestedRole string) (*roleEntry, error) {
|
|
var err error
|
|
role, err := sc.Backend.getRole(sc.Context, sc.Storage, requestedRole)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: err loading role", ErrServerInternal)
|
|
}
|
|
|
|
if role == nil {
|
|
return nil, fmt.Errorf("%w: role does not exist", ErrMalformed)
|
|
}
|
|
|
|
if role.NoStore {
|
|
return nil, fmt.Errorf("%w: role can not be used as NoStore is set to true", ErrServerInternal)
|
|
}
|
|
|
|
return role, nil
|
|
}
|
|
|
|
func getRequestedAcmeRoleFromPath(data *framework.FieldData) string {
|
|
requestedRole := ""
|
|
roleNameRaw, present := data.GetOk("role")
|
|
if present {
|
|
requestedRole = roleNameRaw.(string)
|
|
}
|
|
return requestedRole
|
|
}
|
|
|
|
func getRequestedAcmeIssuerFromPath(data *framework.FieldData) string {
|
|
requestedIssuer := ""
|
|
requestedIssuerRaw, present := data.GetOk(issuerRefParam)
|
|
if present {
|
|
requestedIssuer = requestedIssuerRaw.(string)
|
|
}
|
|
return requestedIssuer
|
|
}
|
|
|
|
func getRequestedPolicyFromPath(data *framework.FieldData) string {
|
|
requestedPolicy := ""
|
|
if requestedPolicyRaw, present := data.GetOk("policy"); present {
|
|
requestedPolicy = requestedPolicyRaw.(string)
|
|
}
|
|
return requestedPolicy
|
|
}
|
|
|
|
func isAcmeDisabled(sc *storageContext, config *acmeConfigEntry, policy EabPolicy) bool {
|
|
if !config.Enabled {
|
|
return true
|
|
}
|
|
|
|
disableAcme, nonFatalErr := isPublicACMEDisabledByEnv()
|
|
if nonFatalErr != nil {
|
|
sc.Backend.Logger().Warn(fmt.Sprintf("could not parse env var '%s'", disableAcmeEnvVar), "error", nonFatalErr)
|
|
}
|
|
|
|
// The OS environment if true will override any configuration option.
|
|
if disableAcme {
|
|
if policy.OverrideEnvDisablingPublicAcme() {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|