detached 5bf3a89

refactor(pki): simplify after code review

- Reuse ValidateDomainParam from pkg/validation in validateDomain
  instead of duplicating control char/path checks
- Restore cache-first in GetCertificate for hot-path performance;
  domain revocation already handled by sweepExpiredCerts every 10m
- Remove redundant comment
commit
5bf3a89200d7a1e1b0d416e17e0cf4909514bf34
author
brice · · 2026-03-30 18:46 UTC
committer
brice · · 2026-03-30 18:48 UTC
parent
450331c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +8 -17
M internal/adapters/out/pki/ca.go +3 -10
@@ -24,6 +24,7 @@ import (
2424 "github.com/bnema/zerowrap"
2525
2626 out "github.com/bnema/gordon/internal/boundaries/out"
27+ "github.com/bnema/gordon/pkg/validation"
2728 )
2829
2930 // Compile-time check: *CA satisfies the CertificateAuthority boundary.
@@ -435,13 +436,8 @@ func parseCertAndKey(certPEM, keyPEM []byte) (*x509.Certificate, crypto.Signer,
435436 // validateDomain performs defense-in-depth validation of a domain name
436437 // before issuing a certificate.
437438 func validateDomain(domain string) error {
438- if domain == "" {
439- return fmt.Errorf("empty domain")
440- }
441- for _, r := range domain {
442- if r < 32 {
443- return fmt.Errorf("domain contains control characters")
444- }
439+ if err := validation.ValidateDomainParam(domain); err != nil {
440+ return err
445441 }
446442 if len(domain) > 253 {
447443 return fmt.Errorf("domain too long (%d chars), limit is 253", len(domain))
@@ -449,9 +445,6 @@ func validateDomain(domain string) error {
449445 if strings.HasPrefix(domain, "*") {
450446 return fmt.Errorf("wildcard domains not allowed")
451447 }
452- if strings.ContainsAny(domain, "/\\") {
453- return fmt.Errorf("domain contains path separators")
454- }
455448 if net.ParseIP(domain) != nil {
456449 return fmt.Errorf("IP addresses not allowed, must be a domain name")
457450 }
@@ -24,6 +24,7 @@ import (
24 "github.com/bnema/zerowrap"24 "github.com/bnema/zerowrap"
25 25
26 out "github.com/bnema/gordon/internal/boundaries/out"26 out "github.com/bnema/gordon/internal/boundaries/out"
27+ "github.com/bnema/gordon/pkg/validation"
27 )28 )
28 29
29 // Compile-time check: *CA satisfies the CertificateAuthority boundary.30 // Compile-time check: *CA satisfies the CertificateAuthority boundary.
@@ -435,13 +436,8 @@ func parseCertAndKey(certPEM, keyPEM []byte) (*x509.Certificate, crypto.Signer,
435 // validateDomain performs defense-in-depth validation of a domain name436 // validateDomain performs defense-in-depth validation of a domain name
436 // before issuing a certificate.437 // before issuing a certificate.
437 func validateDomain(domain string) error {438 func validateDomain(domain string) error {
438- if domain == "" {439+ if err := validation.ValidateDomainParam(domain); err != nil {
439- return fmt.Errorf("empty domain")440+ return err
440- }
441- for _, r := range domain {
442- if r < 32 {
443- return fmt.Errorf("domain contains control characters")
444- }
445 }441 }
446 if len(domain) > 253 {442 if len(domain) > 253 {
447 return fmt.Errorf("domain too long (%d chars), limit is 253", len(domain))443 return fmt.Errorf("domain too long (%d chars), limit is 253", len(domain))
@@ -449,9 +445,6 @@ func validateDomain(domain string) error {
449 if strings.HasPrefix(domain, "*") {445 if strings.HasPrefix(domain, "*") {
450 return fmt.Errorf("wildcard domains not allowed")446 return fmt.Errorf("wildcard domains not allowed")
451 }447 }
452- if strings.ContainsAny(domain, "/\\") {
453- return fmt.Errorf("domain contains path separators")
454- }
455 if net.ParseIP(domain) != nil {448 if net.ParseIP(domain) != nil {
456 return fmt.Errorf("IP addresses not allowed, must be a domain name")449 return fmt.Errorf("IP addresses not allowed, must be a domain name")
457 }450 }
M internal/app/run.go +1 -2
@@ -375,7 +375,6 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
375375 return nil, log.WrapErr(err, "failed to load configuration")
376376 }
377377
378- // Create PKI (internal CA) — disabled when tls_port is 0.
379378 if cfg.Server.TLSPort != 0 {
380379 caAdapter, err := pkiadapter.NewCA(resolveDataDir(cfg.Server.DataDir), log)
381380 if err != nil {
@@ -1908,7 +1907,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services,
19081907 logEvent := log.Info().
19091908 Int("proxy_port", cfg.Server.Port).
19101909 Int("registry_port", cfg.Server.RegistryPort)
1911- if cfg.Server.TLSPort != 0 {
1910+ if tlsSrv != nil {
19121911 logEvent = logEvent.Int("tls_port", cfg.Server.TLSPort)
19131912 }
19141913 logEvent.Msg("Gordon is running")
@@ -375,7 +375,6 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
375 return nil, log.WrapErr(err, "failed to load configuration")375 return nil, log.WrapErr(err, "failed to load configuration")
376 }376 }
377 377
378- // Create PKI (internal CA) — disabled when tls_port is 0.
379 if cfg.Server.TLSPort != 0 {378 if cfg.Server.TLSPort != 0 {
380 caAdapter, err := pkiadapter.NewCA(resolveDataDir(cfg.Server.DataDir), log)379 caAdapter, err := pkiadapter.NewCA(resolveDataDir(cfg.Server.DataDir), log)
381 if err != nil {380 if err != nil {
@@ -1908,7 +1907,7 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, svc *services,
1908 logEvent := log.Info().1907 logEvent := log.Info().
1909 Int("proxy_port", cfg.Server.Port).1908 Int("proxy_port", cfg.Server.Port).
1910 Int("registry_port", cfg.Server.RegistryPort)1909 Int("registry_port", cfg.Server.RegistryPort)
1911- if cfg.Server.TLSPort != 0 {1910+ if tlsSrv != nil {
1912 logEvent = logEvent.Int("tls_port", cfg.Server.TLSPort)1911 logEvent = logEvent.Int("tls_port", cfg.Server.TLSPort)
1913 }1912 }
1914 logEvent.Msg("Gordon is running")1913 logEvent.Msg("Gordon is running")
M internal/usecase/pki/service.go +4 -5
@@ -74,11 +74,6 @@ func (s *Service) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
7474 return nil, fmt.Errorf("no SNI in ClientHello")
7575 }
7676
77- if !s.isDomainAllowed(hello.Context(), domain) {
78- s.cache.Delete(domain)
79- return nil, fmt.Errorf("domain %q not in route table", domain)
80- }
81-
8277 if entry, ok := s.cache.Load(domain); ok {
8378 if cached, ok := entry.(*cachedCert); ok {
8479 if time.Now().Before(cached.expiresAt) {
@@ -88,6 +83,10 @@ func (s *Service) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
8883 s.cache.Delete(domain)
8984 }
9085
86+ if !s.isDomainAllowed(hello.Context(), domain) {
87+ return nil, fmt.Errorf("domain %q not in route table", domain)
88+ }
89+
9190 cert, err := s.ca.IssueCertificate(domain)
9291 if err != nil {
9392 s.log.Error().Err(err).Str("domain", domain).Msg("failed to issue leaf cert")
@@ -74,11 +74,6 @@ func (s *Service) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
74 return nil, fmt.Errorf("no SNI in ClientHello")74 return nil, fmt.Errorf("no SNI in ClientHello")
75 }75 }
76 76
77- if !s.isDomainAllowed(hello.Context(), domain) {
78- s.cache.Delete(domain)
79- return nil, fmt.Errorf("domain %q not in route table", domain)
80- }
81-
82 if entry, ok := s.cache.Load(domain); ok {77 if entry, ok := s.cache.Load(domain); ok {
83 if cached, ok := entry.(*cachedCert); ok {78 if cached, ok := entry.(*cachedCert); ok {
84 if time.Now().Before(cached.expiresAt) {79 if time.Now().Before(cached.expiresAt) {
@@ -88,6 +83,10 @@ func (s *Service) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
88 s.cache.Delete(domain)83 s.cache.Delete(domain)
89 }84 }
90 85
86+ if !s.isDomainAllowed(hello.Context(), domain) {
87+ return nil, fmt.Errorf("domain %q not in route table", domain)
88+ }
89+
91 cert, err := s.ca.IssueCertificate(domain)90 cert, err := s.ca.IssueCertificate(domain)
92 if err != nil {91 if err != nil {
93 s.log.Error().Err(err).Str("domain", domain).Msg("failed to issue leaf cert")92 s.log.Error().Err(err).Str("domain", domain).Msg("failed to issue leaf cert")

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close