This commit is contained in:
parent
747f79a47a
commit
951fdc00c4
@ -104,15 +104,15 @@ type EmailConfig struct {
|
||||
}
|
||||
|
||||
type SSLConfig struct {
|
||||
Management string `json:"management,omitempty"`
|
||||
Mode string `json:"mode"`
|
||||
DefaultDirectory string `json:"defaultDirectory"`
|
||||
CustomDirectory string `json:"customDirectory,omitempty"`
|
||||
Challenge string `json:"challenge,omitempty"`
|
||||
CertificateRef string `json:"certificateRef,omitempty"`
|
||||
PrivateKeyRef string `json:"privateKeyRef,omitempty"`
|
||||
PreviousCertificateRef string `json:"previousCertificateRef,omitempty"`
|
||||
PreviousPrivateKeyRef string `json:"previousPrivateKeyRef,omitempty"`
|
||||
Management string `json:"management,omitempty"`
|
||||
Mode string `json:"mode"`
|
||||
DefaultDirectory string `json:"defaultDirectory"`
|
||||
CustomDirectory string `json:"customDirectory,omitempty"`
|
||||
Challenge string `json:"challenge,omitempty"`
|
||||
CertificateRef string `json:"certificateRef,omitempty"`
|
||||
PrivateKeyRef string `json:"privateKeyRef,omitempty"`
|
||||
PreviousCertificateRef string `json:"previousCertificateRef,omitempty"`
|
||||
PreviousPrivateKeyRef string `json:"previousPrivateKeyRef,omitempty"`
|
||||
DNSProvider string `json:"dnsProvider,omitempty"`
|
||||
DNSZone string `json:"dnsZone,omitempty"`
|
||||
DNSAccessKey StoredSecret `json:"dnsAccessKey,omitempty"`
|
||||
|
||||
@ -15,7 +15,9 @@ func TestDNSCredentialsEncryptedPreservedAndRedacted(t *testing.T) {
|
||||
ROOT_PATH = root
|
||||
t.Cleanup(func() { ROOT_PATH = previousRoot })
|
||||
vault, err := credential.NewVault(root)
|
||||
if err != nil { t.Fatal(err) }
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := validRuntimeConfig()
|
||||
candidate := cloneRuntimeConfig(before)
|
||||
candidate.SSL.TLS.DNSProvider = DNSProviderAliDNS
|
||||
@ -23,29 +25,51 @@ func TestDNSCredentialsEncryptedPreservedAndRedacted(t *testing.T) {
|
||||
"tls.dnsAccessKey": {Operation: SecretReplace, Value: &SecretInput{Plaintext: "dns-access-fixture"}},
|
||||
"tls.dnsSecretKey": {Operation: SecretReplace, Value: &SecretInput{Plaintext: "dns-secret-fixture"}},
|
||||
}
|
||||
if err := applySecretChanges(&candidate, SectionSSL, changes, vault); err != nil { t.Fatal(err) }
|
||||
if err := validateDNSProvider(candidate.SSL.TLS); err != nil { t.Fatal(err) }
|
||||
if err := ValidateRuntimeSecrets(vault, candidate); err != nil { t.Fatal(err) }
|
||||
if err := applySecretChanges(&candidate, SectionSSL, changes, vault); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := validateDNSProvider(candidate.SSL.TLS); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ValidateRuntimeSecrets(vault, candidate); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
access, secret, err := DNSProviderCredentials(candidate.SSL.TLS)
|
||||
if err != nil || access != "dns-access-fixture" || secret != "dns-secret-fixture" { t.Fatal("DNS credentials did not round trip through vault") }
|
||||
if err != nil || access != "dns-access-fixture" || secret != "dns-secret-fixture" {
|
||||
t.Fatal("DNS credentials did not round trip through vault")
|
||||
}
|
||||
data, _ := json.Marshal(candidate)
|
||||
for _, value := range []string{access, secret} { if strings.Contains(string(data), value) { t.Fatal("plaintext persisted") } }
|
||||
for _, value := range []string{access, secret} {
|
||||
if strings.Contains(string(data), value) {
|
||||
t.Fatal("plaintext persisted")
|
||||
}
|
||||
}
|
||||
audit, _ := json.Marshal(AuditChanges(before, candidate))
|
||||
public, _ := json.Marshal(RedactPublicValue(candidate.SSL))
|
||||
for _, data := range [][]byte{audit, public} {
|
||||
for _, value := range []string{access, secret, candidate.SSL.TLS.DNSAccessKey.Ciphertext, candidate.SSL.TLS.DNSSecretKey.Ciphertext} {
|
||||
if strings.Contains(string(data), value) { t.Fatal("DNS secret leaked into audit or public view") }
|
||||
if strings.Contains(string(data), value) {
|
||||
t.Fatal("DNS secret leaked into audit or public view")
|
||||
}
|
||||
}
|
||||
}
|
||||
section := candidate.SSL
|
||||
section.TLS.DNSAccessKey, section.TLS.DNSSecretKey = StoredSecret{}, StoredSecret{}
|
||||
payload, _ := json.Marshal(section)
|
||||
kept, err := applySectionPayload(candidate, SectionSSL, payload, nil, vault)
|
||||
if err != nil || kept.SSL.TLS.DNSAccessKey != candidate.SSL.TLS.DNSAccessKey || kept.SSL.TLS.DNSSecretKey != candidate.SSL.TLS.DNSSecretKey { t.Fatal("public edit lost stored DNS credentials") }
|
||||
if err != nil || kept.SSL.TLS.DNSAccessKey != candidate.SSL.TLS.DNSAccessKey || kept.SSL.TLS.DNSSecretKey != candidate.SSL.TLS.DNSSecretKey {
|
||||
t.Fatal("public edit lost stored DNS credentials")
|
||||
}
|
||||
payload, _ = json.Marshal(candidate.SSL)
|
||||
if _, err := applySectionPayload(candidate, SectionSSL, payload, nil, vault); !errors.Is(err, ErrInvalidSecretChange) { t.Fatal("direct stored-secret injection accepted") }
|
||||
if _, err := applySectionPayload(candidate, SectionSSL, payload, nil, vault); !errors.Is(err, ErrInvalidSecretChange) {
|
||||
t.Fatal("direct stored-secret injection accepted")
|
||||
}
|
||||
clear := map[string]SecretChange{"tls.dnsSecretKey": {Operation: SecretClear, Confirmed: true}}
|
||||
if err := applySecretChanges(&candidate, SectionSSL, clear, vault); !errors.Is(err, ErrSecretClearDenied) { t.Fatal("enabled DNS provider allowed clearing its secret") }
|
||||
if err := applySecretChanges(&candidate, SectionSSL, clear, vault); !errors.Is(err, ErrSecretClearDenied) {
|
||||
t.Fatal("enabled DNS provider allowed clearing its secret")
|
||||
}
|
||||
candidate.SSL.TLS.DNSProvider = ""
|
||||
if err := applySecretChanges(&candidate, SectionSSL, clear, vault); err != nil { t.Fatal(err) }
|
||||
if err := applySecretChanges(&candidate, SectionSSL, clear, vault); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,9 +2,11 @@ package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/utils/atomicfile"
|
||||
)
|
||||
@ -12,6 +14,13 @@ import (
|
||||
// ExportProjectTLS 为本机 TLS 入口导出一个原子更新的证书私钥合并文件。
|
||||
// 仅接受项目生成的平铺证书;不允许高权限调用者按配置读取任意服务器路径。
|
||||
func ExportProjectTLS(root, destination string) error {
|
||||
if runtime.GOOS != "linux" {
|
||||
return errors.New("本机 Nginx 证书同步仅支持 Linux")
|
||||
}
|
||||
if !filepath.IsAbs(root) || !filepath.IsAbs(destination) {
|
||||
return ErrUnsafeSSLFile
|
||||
}
|
||||
root, destination = filepath.Clean(root), filepath.Clean(destination)
|
||||
runtime, err := NewConfigLoader(root, ConfigLoaderOptions{Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: true}}).Load()
|
||||
if err != nil {
|
||||
return ErrInvalidSSL
|
||||
@ -26,43 +35,79 @@ func ExportProjectTLS(root, destination string) error {
|
||||
}
|
||||
sourceDirectory := filepath.Join(root, "ssl")
|
||||
resolvedSource, err := filepath.EvalSymlinks(sourceDirectory)
|
||||
if err != nil || resolvedSource != sourceDirectory { return ErrUnsafeSSLFile }
|
||||
if err != nil || resolvedSource != sourceDirectory {
|
||||
return ErrUnsafeSSLFile
|
||||
}
|
||||
storage, err := os.OpenRoot(sourceDirectory)
|
||||
if err != nil { return ErrUnsafeSSLFile }
|
||||
if err != nil {
|
||||
return ErrUnsafeSSLFile
|
||||
}
|
||||
defer storage.Close()
|
||||
read := func(name string) ([]byte, error) {
|
||||
info, err := storage.Lstat(name)
|
||||
if err != nil || !info.Mode().IsRegular() || info.Size() > maxManagedSSLFileSize || info.Mode().Perm() & 0077 != 0 { return nil, ErrUnsafeSSLFile }
|
||||
if err != nil || !info.Mode().IsRegular() || info.Size() > maxManagedSSLFileSize || info.Mode().Perm()&0077 != 0 {
|
||||
return nil, ErrUnsafeSSLFile
|
||||
}
|
||||
file, err := storage.Open(name)
|
||||
if err != nil { return nil, ErrUnsafeSSLFile }
|
||||
if err != nil {
|
||||
return nil, ErrUnsafeSSLFile
|
||||
}
|
||||
defer file.Close()
|
||||
opened, err := file.Stat()
|
||||
if err != nil || !os.SameFile(info, opened) { return nil, ErrUnsafeSSLFile }
|
||||
if err != nil || !os.SameFile(info, opened) || !opened.Mode().IsRegular() || opened.Mode().Perm()&0077 != 0 {
|
||||
return nil, ErrUnsafeSSLFile
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(file, maxManagedSSLFileSize+1))
|
||||
if err != nil || len(data) > maxManagedSSLFileSize { return nil, ErrUnsafeSSLFile }
|
||||
if err != nil || len(data) > maxManagedSSLFileSize {
|
||||
return nil, ErrUnsafeSSLFile
|
||||
}
|
||||
after, err := file.Stat()
|
||||
if err != nil || !os.SameFile(opened, after) || after.Size() != int64(len(data)) || !opened.ModTime().Equal(after.ModTime()) || after.Mode().Perm()&0077 != 0 {
|
||||
return nil, ErrUnsafeSSLFile
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
certificate, err := read(filepath.Base(publicPath))
|
||||
if err != nil { return err }
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privateKey, err := read(filepath.Base(privatePath))
|
||||
if err != nil { return err }
|
||||
if err := validateSSLMaterial(privateKey, certificate, nil, validationCertificateDomains(&runtime)); err != nil { return err }
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateSSLMaterial(privateKey, certificate, nil, validationCertificateDomains(&runtime)); err != nil {
|
||||
return err
|
||||
}
|
||||
combined := append(append(append([]byte{}, certificate...), '\n'), privateKey...)
|
||||
// 输出目录由部署管理员创建,拒绝符号链接和其他用户可写目录。
|
||||
directory := filepath.Dir(destination)
|
||||
resolved, err := filepath.EvalSymlinks(directory)
|
||||
if err != nil || resolved != directory { return ErrUnsafeSSLFile }
|
||||
if err != nil || resolved != directory {
|
||||
return ErrUnsafeSSLFile
|
||||
}
|
||||
info, err := os.Stat(directory)
|
||||
if err != nil || !info.IsDir() || info.Mode().Perm() & 0022 != 0 { return ErrUnsafeSSLFile }
|
||||
if err != nil || !info.IsDir() || info.Mode().Perm()&0022 != 0 {
|
||||
return ErrUnsafeSSLFile
|
||||
}
|
||||
output, err := os.OpenRoot(directory)
|
||||
if err != nil { return ErrUnsafeSSLFile }
|
||||
if err != nil {
|
||||
return ErrUnsafeSSLFile
|
||||
}
|
||||
defer output.Close()
|
||||
name := filepath.Base(destination)
|
||||
if info, err := output.Lstat(name); err == nil {
|
||||
if !info.Mode().IsRegular() || info.Mode().Perm() & 0077 != 0 { return ErrUnsafeSSLFile }
|
||||
if !info.Mode().IsRegular() || info.Mode().Perm()&0077 != 0 {
|
||||
return ErrUnsafeSSLFile
|
||||
}
|
||||
previous, err := output.ReadFile(name)
|
||||
if err != nil { return ErrUnsafeSSLFile }
|
||||
if bytes.Equal(previous, combined) { return nil }
|
||||
} else if !os.IsNotExist(err) { return ErrUnsafeSSLFile }
|
||||
if err != nil {
|
||||
return ErrUnsafeSSLFile
|
||||
}
|
||||
if bytes.Equal(previous, combined) {
|
||||
return nil
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return ErrUnsafeSSLFile
|
||||
}
|
||||
return atomicfile.WriteRoot(output, name, combined, 0600)
|
||||
}
|
||||
|
||||
@ -5,41 +5,107 @@ import (
|
||||
"crypto/tls"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestExportProjectTLSKeepsOldBundleOnInvalidInput(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
if err := ExportProjectTLS(t.TempDir(), filepath.Join(t.TempDir(), "tls.pem")); err == nil {
|
||||
t.Fatal("unsupported platform allowed exporting a private key")
|
||||
}
|
||||
t.Skip("Linux Nginx export requires POSIX permission checks")
|
||||
}
|
||||
root := t.TempDir()
|
||||
outputDirectory := t.TempDir()
|
||||
if err := os.Chmod(outputDirectory, 0700); err != nil { t.Fatal(err) }
|
||||
if err := os.Chmod(outputDirectory, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
destination := filepath.Join(outputDirectory, "tls.pem")
|
||||
runtime := validRuntimeConfig()
|
||||
runtime.Root.Domains = DomainConfig{Primary: "example.com", Web: "web.example.com", Accepted: []string{"example.com"}}
|
||||
key, certificate := testManagedCertificate(t, "example.com", time.Now().Add(-time.Hour), time.Now().Add(time.Hour), 2048, "*.example.com")
|
||||
ref, err := stageFlatSSLAtRoot(root, ManagedSSLModeDefault, key, certificate, nil, nil)
|
||||
if err != nil { t.Fatal(err) }
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer releaseManagedSSL(root, SSLConfig{CertificateRef: ref.CertificateRef})
|
||||
runtime.SSL.TLS = SSLConfig{Management: SSLManagementProject, Challenge: SSLChallengeDNS, Mode: ManagedSSLModeDefault, DefaultDirectory: "ssl", CertificateRef: ref.CertificateRef, PrivateKeyRef: ref.PrivateKeyRef}
|
||||
writeRuntimeFixture(t, root, runtime)
|
||||
if err := ExportProjectTLS(root, destination); err != nil { t.Fatal(err) }
|
||||
if err := ExportProjectTLS(root, destination); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bundle, err := os.ReadFile(destination)
|
||||
if err != nil { t.Fatal(err) }
|
||||
if _, err := tls.X509KeyPair(bundle, bundle); err != nil { t.Fatal(err) }
|
||||
if err := ExportProjectTLS(root, destination); err != nil { t.Fatal(err) }
|
||||
if err := os.WriteFile(ref.PrivateKeyPath, []byte("invalid"), 0600); err != nil { t.Fatal(err) }
|
||||
if err := ExportProjectTLS(root, destination); err == nil { t.Fatal("invalid key was exported") }
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := tls.X509KeyPair(bundle, bundle); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before, err := os.Stat(destination)
|
||||
if err != nil || before.Mode().Perm() != 0600 {
|
||||
t.Fatal("export must be private")
|
||||
}
|
||||
if err := ExportProjectTLS(root, destination); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after, _ := os.Stat(destination)
|
||||
if !os.SameFile(before, after) {
|
||||
t.Fatal("unchanged certificate must not be rewritten")
|
||||
}
|
||||
for _, target := range []string{ref.PrivateKeyPath, ref.CertificatePath, destination} {
|
||||
if err := os.Chmod(target, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ExportProjectTLS(root, destination); err == nil {
|
||||
t.Fatal("unsafe file permissions accepted")
|
||||
}
|
||||
if err := os.Chmod(target, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.Chmod(outputDirectory, 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ExportProjectTLS(root, destination); err == nil {
|
||||
t.Fatal("writable output directory accepted")
|
||||
}
|
||||
if err := os.Chmod(outputDirectory, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
link := filepath.Join(t.TempDir(), "linked")
|
||||
if err := os.Symlink(outputDirectory, link); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ExportProjectTLS(root, filepath.Join(link, "tls.pem")); err == nil {
|
||||
t.Fatal("linked output directory accepted")
|
||||
}
|
||||
if err := os.WriteFile(ref.PrivateKeyPath, []byte("invalid"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ExportProjectTLS(root, destination); err == nil {
|
||||
t.Fatal("invalid key was exported")
|
||||
}
|
||||
preserved, _ := os.ReadFile(destination)
|
||||
if !bytes.Equal(bundle, preserved) { t.Fatal("failed export replaced valid bundle") }
|
||||
if !bytes.Equal(bundle, preserved) {
|
||||
t.Fatal("failed export replaced valid bundle")
|
||||
}
|
||||
runtime.SSL.TLS.CertificateRef = "ssl/unmanaged.crt"
|
||||
runtime.SSL.TLS.PrivateKeyRef = "ssl/unmanaged.key"
|
||||
writeRuntimeFixture(t, root, runtime)
|
||||
if err := ExportProjectTLS(root, destination); err == nil { t.Fatal("unmanaged input accepted") }
|
||||
if err := ExportProjectTLS(root, destination); err == nil {
|
||||
t.Fatal("unmanaged input accepted")
|
||||
}
|
||||
for _, mode := range []string{SSLManagementCustom, SSLManagementWebServer} {
|
||||
runtime.SSL.TLS.Management = mode
|
||||
writeRuntimeFixture(t, root, runtime)
|
||||
if err := ExportProjectTLS(root, destination); err != nil { t.Fatal(err) }
|
||||
if err := ExportProjectTLS(root, destination); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
preserved, _ := os.ReadFile(destination)
|
||||
if !bytes.Equal(bundle, preserved) { t.Fatal("external mode changed exported bundle") }
|
||||
if !bytes.Equal(bundle, preserved) {
|
||||
t.Fatal("external mode changed exported bundle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ func TestPublicConfigSectionsDoNotExposeSecretsAndRemainStrictlyDecodable(t *tes
|
||||
SSL: config.SSLSectionConfig{
|
||||
Meta: config.SectionMeta{Schema: config.CurrentSectionSchema, Generation: 7},
|
||||
TLS: config.SSLConfig{
|
||||
DNSProvider: config.DNSProviderAliDNS,
|
||||
DNSProvider: config.DNSProviderAliDNS,
|
||||
DNSAccessKey: config.StoredSecret{Ciphertext: "enc:v1:dns-access", Version: credential.CurrentVersion, UpdatedAt: time.Now().UTC()},
|
||||
DNSSecretKey: config.StoredSecret{Ciphertext: "enc:v1:dns-secret", Version: credential.CurrentVersion, UpdatedAt: time.Now().UTC()},
|
||||
},
|
||||
|
||||
@ -94,7 +94,9 @@ func (provider *aliDNSChallenge) Present(domain, token, keyAuth string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var response struct{ RecordID string `json:"RecordId"` }
|
||||
var response struct {
|
||||
RecordID string `json:"RecordId"`
|
||||
}
|
||||
err = provider.call("AddDomainRecord", url.Values{
|
||||
"DomainName": {provider.zone}, "RR": {record.Name}, "Type": {"TXT"},
|
||||
"Value": {record.Value}, "TTL": {"600"},
|
||||
@ -174,7 +176,9 @@ func (provider *aliDNSChallenge) call(action string, values url.Values, target a
|
||||
if err != nil || len(data) > 1<<20 {
|
||||
return errors.New("DNS 服务商响应无效")
|
||||
}
|
||||
var status struct{ Code string `json:"Code"` }
|
||||
var status struct {
|
||||
Code string `json:"Code"`
|
||||
}
|
||||
if json.Unmarshal(data, &status) != nil || response.StatusCode != http.StatusOK || status.Code != "" {
|
||||
// 服务商错误可能包含凭据、签名或请求原文,不能直接交给日志。
|
||||
return errors.New("DNS 服务商拒绝请求,请检查凭据、区域和记录管理权限")
|
||||
|
||||
@ -35,10 +35,16 @@ func TestAutomaticDNSCleansOnlyOwnedUnchangedRecords(t *testing.T) {
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"RecordId": id})
|
||||
case "DescribeDomainRecordInfo":
|
||||
record := records[id]
|
||||
if tamper { record.Value = "changed-by-user" }
|
||||
if tamper {
|
||||
record.Value = "changed-by-user"
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"RecordId": id, "DomainName": "example.com", "RR": record.Name, "Type": "TXT", "Value": record.Value})
|
||||
case "DeleteDomainRecord":
|
||||
if failDelete { w.WriteHeader(503); _, _ = w.Write([]byte(`{"Code":"Busy"}`)); return }
|
||||
if failDelete {
|
||||
w.WriteHeader(503)
|
||||
_, _ = w.Write([]byte(`{"Code":"Busy"}`))
|
||||
return
|
||||
}
|
||||
deletes++
|
||||
delete(records, id)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"RecordId": id})
|
||||
@ -55,23 +61,50 @@ func TestAutomaticDNSCleansOnlyOwnedUnchangedRecords(t *testing.T) {
|
||||
workers.Add(1)
|
||||
go func(domain string) {
|
||||
defer workers.Done()
|
||||
if err := provider.Present(domain, "token", "key-"+domain); err != nil { t.Error(err) }
|
||||
if err := provider.Present(domain, "token", "key-"+domain); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}(domain)
|
||||
}
|
||||
}
|
||||
workers.Wait()
|
||||
if adds != 2 || len(records) != 3 { t.Fatalf("same-name TXT records lost or duplicated: adds=%d records=%d", adds, len(records)) }
|
||||
if err := provider.CleanUp("other.example.com", "token", "unknown"); err != nil || deletes != 0 { t.Fatal("unknown challenge must not touch DNS") }
|
||||
mu.Lock(); tamper = true; mu.Unlock()
|
||||
if err := provider.CleanUp("example.com", "token", "key-example.com"); err == nil || deletes != 0 { t.Fatal("modified record must be retained") }
|
||||
mu.Lock(); tamper = false; failDelete = true; mu.Unlock()
|
||||
if err := provider.CleanUp("example.com", "token", "key-example.com"); err == nil || len(provider.records) != 2 { t.Fatal("failed deletion must retain ownership for retry") }
|
||||
mu.Lock(); failDelete = false; mu.Unlock()
|
||||
if err := provider.CleanUp("example.com", "token", "key-example.com"); err != nil { t.Fatal(err) }
|
||||
if len(records) != 2 { t.Fatal("cleaning root challenge removed wildcard or unrelated TXT") }
|
||||
if err := provider.CleanUp("*.example.com", "token", "key-*.example.com"); err != nil { t.Fatal(err) }
|
||||
if len(records) != 1 || records["existing"].Value != "unrelated-record" { t.Fatal("existing TXT must survive all challenge cleanup") }
|
||||
if err := provider.CleanUp("*.example.com", "token", "key-*.example.com"); err != nil || deletes != 2 { t.Fatal("cleanup must be idempotent") }
|
||||
if adds != 2 || len(records) != 3 {
|
||||
t.Fatalf("same-name TXT records lost or duplicated: adds=%d records=%d", adds, len(records))
|
||||
}
|
||||
if err := provider.CleanUp("other.example.com", "token", "unknown"); err != nil || deletes != 0 {
|
||||
t.Fatal("unknown challenge must not touch DNS")
|
||||
}
|
||||
mu.Lock()
|
||||
tamper = true
|
||||
mu.Unlock()
|
||||
if err := provider.CleanUp("example.com", "token", "key-example.com"); err == nil || deletes != 0 {
|
||||
t.Fatal("modified record must be retained")
|
||||
}
|
||||
mu.Lock()
|
||||
tamper = false
|
||||
failDelete = true
|
||||
mu.Unlock()
|
||||
if err := provider.CleanUp("example.com", "token", "key-example.com"); err == nil || len(provider.records) != 2 {
|
||||
t.Fatal("failed deletion must retain ownership for retry")
|
||||
}
|
||||
mu.Lock()
|
||||
failDelete = false
|
||||
mu.Unlock()
|
||||
if err := provider.CleanUp("example.com", "token", "key-example.com"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(records) != 2 {
|
||||
t.Fatal("cleaning root challenge removed wildcard or unrelated TXT")
|
||||
}
|
||||
if err := provider.CleanUp("*.example.com", "token", "key-*.example.com"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(records) != 1 || records["existing"].Value != "unrelated-record" {
|
||||
t.Fatal("existing TXT must survive all challenge cleanup")
|
||||
}
|
||||
if err := provider.CleanUp("*.example.com", "token", "key-*.example.com"); err != nil || deletes != 2 {
|
||||
t.Fatal("cleanup must be idempotent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutomaticDNSRejectsOutsideZoneAndRedactsErrors(t *testing.T) {
|
||||
@ -84,13 +117,21 @@ func TestAutomaticDNSRejectsOutsideZoneAndRedactsErrors(t *testing.T) {
|
||||
}))
|
||||
defer server.Close()
|
||||
provider := &aliDNSChallenge{zone: "example.com", accessKey: "test-access", secretKey: "test-secret", endpoint: server.URL, client: server.Client(), records: map[string]aliDNSRecord{}}
|
||||
if err := provider.Present("unrelated.com", "token", "key"); err == nil || calls != 0 { t.Fatal("out-of-zone write attempted") }
|
||||
if err := provider.Present("unrelated.com", "token", "key"); err == nil || calls != 0 {
|
||||
t.Fatal("out-of-zone write attempted")
|
||||
}
|
||||
err := provider.Present("example.com", "token", "key")
|
||||
if err == nil || calls != 1 { t.Fatal("provider refusal not reported") }
|
||||
if err == nil || calls != 1 {
|
||||
t.Fatal("provider refusal not reported")
|
||||
}
|
||||
for _, secret := range []string{"test-access", "test-secret", "signature"} {
|
||||
if strings.Contains(err.Error(), secret) { t.Fatal("error leaked provider response") }
|
||||
if strings.Contains(err.Error(), secret) {
|
||||
t.Fatal("error leaked provider response")
|
||||
}
|
||||
}
|
||||
for _, management := range []string{config.SSLManagementCustom, config.SSLManagementWebServer} {
|
||||
if _, err := automaticDNSProvider(&config.Config{SSL: config.SSLConfig{Management: management, Challenge: config.SSLChallengeDNS}}); err == nil { t.Fatal("external certificate mode allowed automatic DNS") }
|
||||
if _, err := automaticDNSProvider(&config.Config{SSL: config.SSLConfig{Management: management, Challenge: config.SSLChallengeDNS}}); err == nil {
|
||||
t.Fatal("external certificate mode allowed automatic DNS")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,10 +118,10 @@ func SetSSL(sslType, priKey, crtKey string) error {
|
||||
Management: config.SSLManagementProject,
|
||||
Mode: config.ManagedSSLModeDefault,
|
||||
DefaultDirectory: runtimepath.SSLReference(config.ROOT_PATH, "data/ssl"),
|
||||
DNSProvider: previousSSL.DNSProvider,
|
||||
DNSZone: previousSSL.DNSZone,
|
||||
DNSAccessKey: previousSSL.DNSAccessKey,
|
||||
DNSSecretKey: previousSSL.DNSSecretKey,
|
||||
DNSProvider: previousSSL.DNSProvider,
|
||||
DNSZone: previousSSL.DNSZone,
|
||||
DNSAccessKey: previousSSL.DNSAccessKey,
|
||||
DNSSecretKey: previousSSL.DNSSecretKey,
|
||||
}
|
||||
if cfg.SSLType == config.SSLTypeAutoDNS {
|
||||
cfg.SSL.Challenge = config.SSLChallengeDNS
|
||||
@ -192,10 +192,10 @@ func applyManagedSSLReference(cfg *config.Config, reference config.ManagedSSLRef
|
||||
DefaultDirectory: "ssl",
|
||||
CertificateRef: reference.CertificateRef,
|
||||
PrivateKeyRef: reference.PrivateKeyRef,
|
||||
DNSProvider: previousSSL.DNSProvider,
|
||||
DNSZone: previousSSL.DNSZone,
|
||||
DNSAccessKey: previousSSL.DNSAccessKey,
|
||||
DNSSecretKey: previousSSL.DNSSecretKey,
|
||||
DNSProvider: previousSSL.DNSProvider,
|
||||
DNSZone: previousSSL.DNSZone,
|
||||
DNSAccessKey: previousSSL.DNSAccessKey,
|
||||
DNSSecretKey: previousSSL.DNSSecretKey,
|
||||
}
|
||||
if reference.Mode == config.ManagedSSLModeUpload {
|
||||
cfg.SSL.CustomDirectory = "ssl"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user