Option to allow public contact email addresses in lockdown mode

When in lockdown mode, only those domains can be used to request certificates for,
but it also only accepts email addresses in those domains. With this option in the
GUI it is now possible to still allow all public domains in contact addresses.
This commit is contained in:
Arjan H
2024-02-04 13:46:26 +01:00
parent 10c3d6ad6e
commit 045a128c2c
6 changed files with 103 additions and 27 deletions

View File

@@ -86,6 +86,15 @@ if [ "$PKI_DOMAIN_MODE" == "lockdown" ] && [ "$PKI_LOCKDOWN_DOMAINS" != "" ]; th
for d in $(echo $PKI_LOCKDOWN_DOMAINS | sed -e "s/\\\r/ /g" | sed -e "s/\\\n/ /g" | tr '\r' ' '); do
echo " - \"$d\"" >> hostname-policy.yaml
done
allow_public="false"
ld_public_contacts=$(grep ld_public_contacts $dataDir/config.json | grep true || echo "")
if [ "$ld_public_contacts" != "" ]; then
allow_public="true"
fi
echo >> hostname-policy.yaml
echo "LockdownAllowPublicContacts: $allow_public" >> hostname-policy.yaml
fi
if [ "$PKI_DOMAIN_MODE" == "whitelist" ] && [ "$PKI_WHITELIST_DOMAINS" != "" ]; then
echo >> hostname-policy.yaml

View File

@@ -176,6 +176,7 @@ type SetupConfig struct {
DomainMode string
LockdownDomains string
WhitelistDomains string
LDPublicContacts bool
ExtendedTimeout bool
RequestBase string
Errors map[string]string
@@ -666,6 +667,7 @@ func _configUpdateHandler(w http.ResponseWriter, r *http.Request) {
DomainMode: r.Form.Get("domain_mode"),
LockdownDomains: r.Form.Get("lockdown_domains"),
WhitelistDomains: r.Form.Get("whitelist_domains"),
LDPublicContacts: (r.Form.Get("ld_public_contacts") == "true"),
ExtendedTimeout: (r.Form.Get("extended_timeout") == "true"),
}
@@ -715,6 +717,11 @@ func _configUpdateHandler(w http.ResponseWriter, r *http.Request) {
delta = true
viper.Set("labca.lockdown", cfg.LockdownDomains)
}
if cfg.LDPublicContacts != viper.GetBool("labca.ld_public_contacts") {
delta = true
viper.Set("labca.ld_public_contacts", cfg.LDPublicContacts)
}
}
if domainMode == "whitelist" {
if cfg.WhitelistDomains != viper.GetString("labca.whitelist") {
@@ -1585,6 +1592,7 @@ func _manageGet(w http.ResponseWriter, r *http.Request) {
manageData["DomainMode"] = domainMode
if domainMode == "lockdown" {
manageData["LockdownDomains"] = viper.GetString("labca.lockdown")
manageData["LDPublicContacts"] = viper.GetBool("labca.ld_public_contacts")
}
if domainMode == "whitelist" {
manageData["WhitelistDomains"] = viper.GetString("labca.whitelist")
@@ -2536,6 +2544,7 @@ func _setupBaseConfig(w http.ResponseWriter, r *http.Request) bool {
DomainMode: "lockdown",
LockdownDomains: domain,
WhitelistDomains: domain,
LDPublicContacts: true,
RequestBase: r.Header.Get("X-Request-Base"),
}
@@ -2553,6 +2562,7 @@ func _setupBaseConfig(w http.ResponseWriter, r *http.Request) bool {
DomainMode: r.Form.Get("domain_mode"),
LockdownDomains: r.Form.Get("lockdown_domains"),
WhitelistDomains: r.Form.Get("whitelist_domains"),
LDPublicContacts: (r.Form.Get("ld_public_contacts") == "true"),
RequestBase: r.Header.Get("X-Request-Base"),
}
@@ -2571,6 +2581,7 @@ func _setupBaseConfig(w http.ResponseWriter, r *http.Request) bool {
viper.Set("labca.domain_mode", cfg.DomainMode)
if cfg.DomainMode == "lockdown" {
viper.Set("labca.lockdown", cfg.LockdownDomains)
viper.Set("labca.ld_public_contacts", cfg.LDPublicContacts)
}
if cfg.DomainMode == "whitelist" {
viper.Set("labca.whitelist", cfg.WhitelistDomains)

View File

@@ -165,8 +165,10 @@ $(function() {
if ($("input[type=radio]#whitelist").prop('checked') || $("input[type=radio]#standard").prop('checked') ) {
$("#domain_mode_warning").show();
$("#ld_options").hide();
} else {
$("#domain_mode_warning").hide();
$("#ld_options").show();
}
}
@@ -175,6 +177,7 @@ $(function() {
});
$("#domain_mode_warning").hide();
$("#ld_options").show();
radioDisable();

View File

@@ -295,17 +295,26 @@
<span class="error config-error hidden" id="whitelistdomains-error"></span>
<br/>
<input type="radio" id="standard" name="domain_mode" value="standard" {{ if eq .DomainMode "standard"}}checked{{ end }}/> Standard - any official domains<br/>
<input type="radio" id="standard" name="domain_mode" value="standard" {{ if eq .DomainMode "standard"}}checked{{ end }}/> Standard - any official domains
</div>
<div class="form-group" id="ld_options">
<label>Lockdown options:</label><br/>
<input type="checkbox" id="ld_public_contacts" name="ld_public_contacts" value="true" {{ if .LDPublicContacts }}checked{{ end }}></input>
&nbsp;Still allow all public domain names in contact email addresses when creating new ACME accounts
</div>
<div class="form-group">
<label>Extended timeout:</label><br/>
<input type="checkbox" class="extended_timeout" id="extended_timeout" value="extend" {{ if .ExtendedTimeout }}checked{{ end }}></input>
&nbsp;If you see timeout related errors on the Dashboard / Audit Log, try checking this box.
</div>
<div class="form-group" id="domain_mode_warning">
<img src="static/img/warning.png"> Are you sure? This facilitates man-in-the-middle attacks!
</div>
<div class="form-group">
<span class="hidden" id="update-config-result"></span>
<button class="btn btn-default" type="button" id="update-config" title="Update the system configuration">Update</button>
<span id="domain_mode_warning">&nbsp;<img src="static/img/warning.png"> Are you sure? This facilitates man-in-the-middle attacks!<br/></span>
</div>
</form>
</div>
@@ -1001,6 +1010,7 @@
domain_mode: ($("#standard").prop('checked') ? 'standard' : ($("#whitelist").prop('checked') ? 'whitelist' : 'lockdown')),
lockdown_domains: $("#lockdown_domains").val(),
whitelist_domains: $("#whitelist_domains").val(),
ld_public_contacts: $("#ld_public_contacts").prop("checked"),
extended_timeout: $("#extended_timeout").prop("checked"),
},
})

View File

@@ -37,11 +37,18 @@
<span class="error">{{ . }}</span><br/>
{{ end }}
<input type="radio" id="standard" name="domain_mode" value="standard" {{ if eq .DomainMode "standard"}}checked{{ end }}/> Standard - any official domains<br/><br/>
<input type="radio" id="standard" name="domain_mode" value="standard" {{ if eq .DomainMode "standard"}}checked{{ end }}/> Standard - any official domains<br/>
</div>
<div class="form-group" id="ld_options">
<label>Lockdown options:</label><br/>
<input type="checkbox" id="ld_public_contacts" name="ld_public_contacts" value="true" {{ if .LDPublicContacts }}checked{{ end }}></input>
&nbsp;Still allow all public domain names in contact email addresses when creating new ACME accounts
</div>
<div class="form-group" id="domain_mode_warning">
<img src="static/img/warning.png"> Are you sure? This facilitates man-in-the-middle attacks!
</div>
<div class="form-group">
<input class="btn btn-default" type="submit" value="Create">
<span id="domain_mode_warning">&nbsp;<img src="static/img/warning.png"> Are you sure? This facilitates man-in-the-middle attacks!</span><br/>
</div>
</form>
{{end}}

View File

@@ -1,27 +1,29 @@
diff --git a/policy/pa.go b/policy/pa.go
index d872d5cbe..49daeb7d4 100644
index d872d5cbe..faa052d6c 100644
--- a/policy/pa.go
+++ b/policy/pa.go
@@ -32,6 +32,8 @@ type AuthorityImpl struct {
@@ -32,6 +32,9 @@ type AuthorityImpl struct {
blocklist map[string]bool
exactBlocklist map[string]bool
wildcardExactBlocklist map[string]bool
+ whitelist map[string]bool
+ lockdown map[string]bool
+ ldPublicContacts bool
blocklistMu sync.RWMutex
enabledChallenges map[core.AcmeChallenge]bool
@@ -72,6 +74,9 @@ type blockedNamesPolicy struct {
@@ -72,6 +75,10 @@ type blockedNamesPolicy struct {
// time above and beyond the high-risk domains. Managing these entries separately
// from HighRiskBlockedNames makes it easier to vet changes accurately.
AdminBlockedNames []string `yaml:"AdminBlockedNames"`
+
+ Whitelist []string `yaml:"Whitelist"`
+ Lockdown []string `yaml:"Lockdown"`
+ Whitelist []string `yaml:"Whitelist"`
+ Lockdown []string `yaml:"Lockdown"`
+ LockdownAllowPublicContacts bool `yaml:"LockdownAllowPublicContacts"`
}
// LoadHostnamePolicyFile will load the given policy file, returning an error if
@@ -131,10 +136,20 @@ func (pa *AuthorityImpl) processHostnamePolicy(policy blockedNamesPolicy) error
@@ -131,10 +138,21 @@ func (pa *AuthorityImpl) processHostnamePolicy(policy blockedNamesPolicy) error
// wildcardNameMap to block issuance for `*.`+parts[1]
wildcardNameMap[parts[1]] = true
}
@@ -39,23 +41,35 @@ index d872d5cbe..49daeb7d4 100644
pa.wildcardExactBlocklist = wildcardNameMap
+ pa.whitelist = whiteMap
+ pa.lockdown = lockMap
+ pa.ldPublicContacts = policy.LockdownAllowPublicContacts
pa.blocklistMu.Unlock()
return nil
}
@@ -203,7 +218,7 @@ var (
@@ -203,7 +221,7 @@ var (
// - exactly equal to an IANA registered TLD
//
// It does NOT ensure that the domain is absent from any PA blocked lists.
-func ValidNonWildcardDomain(domain string) error {
+func (pa *AuthorityImpl) ValidNonWildcardDomain(domain string) error {
+func (pa *AuthorityImpl) ValidNonWildcardDomain(domain string, isContact bool) error {
if domain == "" {
return errEmptyName
}
@@ -279,6 +294,14 @@ func ValidNonWildcardDomain(domain string) error {
@@ -235,7 +253,9 @@ func ValidNonWildcardDomain(domain string) error {
return errTooManyLabels
}
if len(labels) < 2 {
- return errTooFewLabels
+ if !pa.lockdown[domain] && !pa.whitelist[domain] {
+ return errTooFewLabels
+ }
}
for _, label := range labels {
// Check that this is a valid LDH Label: "A string consisting of ASCII
@@ -279,6 +299,14 @@ func ValidNonWildcardDomain(domain string) error {
}
}
+ ok, err := pa.checkWhitelist(domain)
+ ok, err := pa.checkWhitelist(domain, isContact)
+ if err != nil {
+ return err
+ }
@@ -66,7 +80,7 @@ index d872d5cbe..49daeb7d4 100644
// Names must end in an ICANN TLD, but they must not be equal to an ICANN TLD.
icannTLD, err := iana.ExtractSuffix(domain)
if err != nil {
@@ -294,9 +317,9 @@ func ValidNonWildcardDomain(domain string) error {
@@ -294,9 +322,9 @@ func ValidNonWildcardDomain(domain string) error {
// ValidDomain checks that a domain is valid and that it doesn't contain any
// invalid wildcard characters. It does NOT ensure that the domain is absent
// from any PA blocked lists.
@@ -74,20 +88,29 @@ index d872d5cbe..49daeb7d4 100644
+func (pa *AuthorityImpl) ValidDomain(domain string) error {
if strings.Count(domain, "*") <= 0 {
- return ValidNonWildcardDomain(domain)
+ return pa.ValidNonWildcardDomain(domain)
+ return pa.ValidNonWildcardDomain(domain, false)
}
// Names containing more than one wildcard are invalid.
@@ -323,7 +346,7 @@ func ValidDomain(domain string) error {
@@ -315,7 +343,7 @@ func ValidDomain(domain string) error {
// Names must end in an ICANN TLD, but they must not be equal to an ICANN TLD.
icannTLD, err := iana.ExtractSuffix(baseDomain)
- if err != nil {
+ if err != nil && !pa.lockdown[baseDomain] && !pa.whitelist[baseDomain] {
return errNonPublic
}
// Names must have a non-wildcard label immediately adjacent to the ICANN
@@ -323,7 +351,7 @@ func ValidDomain(domain string) error {
if baseDomain == icannTLD {
return errICANNTLDWildcard
}
- return ValidNonWildcardDomain(baseDomain)
+ return pa.ValidNonWildcardDomain(baseDomain)
+ return pa.ValidNonWildcardDomain(baseDomain, false)
}
// forbiddenMailDomains is a map of domain names we do not allow after the
@@ -341,7 +364,7 @@ var forbiddenMailDomains = map[string]bool{
@@ -341,7 +369,7 @@ var forbiddenMailDomains = map[string]bool{
// ValidEmail returns an error if the input doesn't parse as an email address,
// the domain isn't a valid hostname in Preferred Name Syntax, or its on the
// list of domains forbidden for mail (because they are often used in examples).
@@ -96,16 +119,16 @@ index d872d5cbe..49daeb7d4 100644
email, err := mail.ParseAddress(address)
if err != nil {
if len(address) > 254 {
@@ -351,7 +374,7 @@ func ValidEmail(address string) error {
@@ -351,7 +379,7 @@ func ValidEmail(address string) error {
}
splitEmail := strings.SplitN(email.Address, "@", -1)
domain := strings.ToLower(splitEmail[len(splitEmail)-1])
- err = ValidNonWildcardDomain(domain)
+ err = pa.ValidNonWildcardDomain(domain)
+ err = pa.ValidNonWildcardDomain(domain, true)
if err != nil {
return berrors.InvalidEmailError(
"contact email %q has invalid domain : %s",
@@ -416,7 +439,7 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error {
@@ -416,7 +444,7 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error {
for _, domain := range domains {
if strings.Count(domain, "*") > 0 {
// Domain contains a wildcard, check that it is valid.
@@ -114,28 +137,28 @@ index d872d5cbe..49daeb7d4 100644
if err != nil {
appendSubError(domain, err)
continue
@@ -433,12 +456,15 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error {
@@ -433,12 +461,15 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error {
}
} else {
// Validate that the domain is well-formed.
- err := ValidNonWildcardDomain(domain)
+ err := pa.ValidNonWildcardDomain(domain)
+ err := pa.ValidNonWildcardDomain(domain, false)
if err != nil {
appendSubError(domain, err)
continue
}
}
+ if ok, _ := pa.checkWhitelist(domain); ok {
+ if ok, _ := pa.checkWhitelist(domain, false); ok {
+ return nil
+ }
// Require no match against hostname block lists
err := pa.checkHostLists(domain)
if err != nil {
@@ -471,6 +497,31 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error {
@@ -471,6 +502,34 @@ func (pa *AuthorityImpl) WillingToIssue(domains []string) error {
return nil
}
+func (pa *AuthorityImpl) checkWhitelist(domain string) (bool, error) {
+func (pa *AuthorityImpl) checkWhitelist(domain string, isContact bool) (bool, error) {
+ pa.blocklistMu.RLock()
+ defer pa.blocklistMu.RUnlock()
+
@@ -152,6 +175,9 @@ index d872d5cbe..49daeb7d4 100644
+ }
+
+ if len(pa.lockdown) > 0 {
+ if isContact && pa.ldPublicContacts {
+ return false, nil
+ }
+ // In Lockdown mode, the domain MUST be in the list, so return an error if not found
+ return false, errPolicyForbidden
+ } else {
@@ -163,3 +189,13 @@ index d872d5cbe..49daeb7d4 100644
// checkWildcardHostList checks the wildcardExactBlocklist for a given domain.
// If the domain is not present on the list nil is returned, otherwise
// errPolicyForbidden is returned.
@@ -500,6 +559,9 @@ func (pa *AuthorityImpl) checkHostLists(domain string) error {
labels := strings.Split(domain, ".")
for i := range labels {
joined := strings.Join(labels[i:], ".")
+ if pa.lockdown[domain] {
+ continue
+ }
if pa.blocklist[joined] {
return errPolicyForbidden
}