remove tls cache

rabbitprincess committed Mar 2, 2026 at 18:46 UTC 45e0847d57489670b121b9f06de133fd40a06e08
8 files changed +25 -203
AGENTS.md
-1
@@ -90,7 +90,6 @@ Portal uses environment variables for domain and TLS configuration:
90 | `TLS_MODE` | `no-tls`, `self`, or `keyless` |
91 | `TLS_CERT_FILE` | Self TLS certificate chain path (self mode only) |
92 | `TLS_KEY_FILE` | Self TLS private key path (self mode only) |
93 -| `TLS_CERT_CACHE_FILE` | Keyless cert cache file path (keyless mode only) |
93
94 ### Domain Derivation
95
cmd/demo-app/main.go
+1 -1
@@ -40,7 +40,7 @@ var (
40 )
41
42 func main() {
43 - flag.StringVar(&flagServerURL, "server-url", "http://localhost:4017", "relay API URL (http/https)")
43 + flag.StringVar(&flagServerURL, "server-url", "https://gosunuts.xyz", "relay API URL (http/https)")
44 flag.IntVar(&flagPort, "port", 8092, "local demo HTTP port")
45 flag.StringVar(&flagName, "name", "demo-app", "backend display name")
46 flag.StringVar(&flagDesc, "description", "Portal demo connectivity app", "lease description")
cmd/portal-tunnel/README.md
+1 -4
@@ -53,7 +53,6 @@ Options:
53 --tls-mode TLS mode: no-tls, self, or keyless [default: no-tls] [env: TLS_MODE]
54 --tls-cert-file PEM certificate chain for --tls-mode self [env: TLS_CERT_FILE]
55 --tls-key-file PEM private key for --tls-mode self [env: TLS_KEY_FILE]
56 - --tls-cert-cache-file Certificate cache file path for --tls-mode keyless [env: TLS_CERT_CACHE_FILE]
56 --description Service description metadata [env: APP_DESCRIPTION]
57 --tags Service tags metadata (comma-separated) [env: APP_TAGS]
58 --thumbnail Service thumbnail URL metadata [env: APP_THUMBNAIL]
@@ -81,7 +80,6 @@ export RELAYS=https://portal.example.com
80 export APP_HOST=localhost:3000
81 export APP_NAME=myapp
82 export TLS_MODE=keyless
84 -export TLS_CERT_CACHE_FILE=/var/lib/portal/cache/cert-cache.json
83
84 ./bin/portal-tunnel
85 ```
@@ -106,7 +104,6 @@ export RELAYS=https://portal.example.com
104 export APP_HOST=localhost:3000
105 export APP_NAME=myapp
106 export TLS_MODE=keyless
109 -export TLS_CERT_CACHE_FILE=/var/lib/portal/cache/cert-cache.json
107
108 ./bin/portal-tunnel
109 ```
@@ -139,4 +136,4 @@ Expected signer API contract (`/v1/sign`):
136 --name myapp \
137 --relay https://portal1.example.com,https://portal2.example.com \
138 --tls-mode keyless
142 -```
\ No newline at end of file
139 +```
cmd/portal-tunnel/main.go
+12 -22
@@ -16,23 +16,21 @@ import (
16 "github.com/rs/zerolog"
17 "github.com/rs/zerolog/log"
18
19 - "gosuda.org/portal/portal/acme"
19 "gosuda.org/portal/sdk"
20 )
21
22 var (
24 - flagRelayURLs string
25 - flagHost string
26 - flagName string
27 - flagTLSMode string
28 - flagTLSCertFile string
29 - flagTLSKeyFile string
30 - flagTLSCertCacheFile string
31 - flagDescription string
32 - flagTags string
33 - flagThumbnail string
34 - flagOwner string
35 - flagHide bool
23 + flagRelayURLs string
24 + flagHost string
25 + flagName string
26 + flagTLSMode string
27 + flagTLSCertFile string
28 + flagTLSKeyFile string
29 + flagDescription string
30 + flagTags string
31 + flagThumbnail string
32 + flagOwner string
33 + flagHide bool
34 )
35
36 func main() {
@@ -54,7 +52,6 @@ func main() {
52 flag.StringVar(&flagTLSMode, "tls-mode", defaultTLSMode, "TLS mode: no-tls, self, or keyless [env: TLS_MODE]")
53 flag.StringVar(&flagTLSCertFile, "tls-cert-file", os.Getenv("TLS_CERT_FILE"), "PEM certificate chain for --tls-mode self [env: TLS_CERT_FILE]")
54 flag.StringVar(&flagTLSKeyFile, "tls-key-file", os.Getenv("TLS_KEY_FILE"), "PEM private key for --tls-mode self [env: TLS_KEY_FILE]")
57 - flag.StringVar(&flagTLSCertCacheFile, "tls-cert-cache-file", os.Getenv("TLS_CERT_CACHE_FILE"), "certificate cache file path for --tls-mode keyless [env: TLS_CERT_CACHE_FILE]")
55
56 flag.StringVar(&flagDescription, "description", os.Getenv("APP_DESCRIPTION"), "Service description metadata [env: APP_DESCRIPTION]")
57 flag.StringVar(&flagTags, "tags", os.Getenv("APP_TAGS"), "Service tags metadata (comma-separated) [env: APP_TAGS]")
@@ -133,21 +130,14 @@ func runServiceTunnel(ctx context.Context, relayURLs []string) error {
130 Str("key_file", keyFile).
131 Msg("TLS: Using self-managed local certificate")
132 } else if flagTLSMode == string(sdk.TLSModeKeyless) {
136 - certCacheFile := strings.TrimSpace(flagTLSCertCacheFile)
137 - if certCacheFile == "" {
138 - certCacheFile = acme.DefaultCertCachePath()
139 - }
133 certFile := strings.TrimSpace(flagTLSCertFile)
134 if certFile != "" {
135 log.Warn().
136 Str("cert_file", certFile).
137 Msg("Ignoring --tls-cert-file in keyless mode (SDK auto configuration only)")
138 }
146 - clientOpts = append(clientOpts, sdk.WithCertCacheFile(certCacheFile))
139 clientOpts = append(clientOpts, sdk.WithTLSKeylessDefaults())
148 - log.Info().
149 - Str("cert_cache_file", certCacheFile).
150 - Msg("TLS: Using keyless remote signer (SDK auto configuration)")
140 + log.Info().Msg("TLS: Using keyless remote signer (SDK auto configuration)")
141 } else {
142 return fmt.Errorf("unsupported TLS mode: %s", flagTLSMode)
143 }
portal/acme/certcache.go deleted
-130
@@ -1,130 +0,0 @@
1 -package acme
2 -
3 -import (
4 - "crypto/x509"
5 - "encoding/json"
6 - "encoding/pem"
7 - "errors"
8 - "fmt"
9 - "os"
10 - "path/filepath"
11 - "time"
12 -
13 - "github.com/rs/zerolog/log"
14 -)
15 -
16 -const (
17 - // DefaultCacheTTL is the default time-to-live for cached certificates.
18 - DefaultCacheTTL = 24 * time.Hour
19 -
20 - // ExpiryThreshold is how long before cert expiration to refresh cache.
21 - ExpiryThreshold = 30 * 24 * time.Hour
22 -
23 - // DefaultCacheFileName is the default filename for certificate cache.
24 - DefaultCacheFileName = "portal-cert-cache.json"
25 -)
26 -
27 -// DefaultCertCachePath returns the default path for certificate cache.
28 -// It uses the OS-specific user cache directory if available, otherwise
29 -// falls back to the current directory.
30 -func DefaultCertCachePath() string {
31 - if cacheDir, err := os.UserCacheDir(); err == nil {
32 - return filepath.Join(cacheDir, "portal", DefaultCacheFileName)
33 - }
34 - if homeDir, err := os.UserHomeDir(); err == nil {
35 - return filepath.Join(homeDir, ".cache", "portal", DefaultCacheFileName)
36 - }
37 - return DefaultCacheFileName
38 -}
39 -
40 -// CertCacheEntry represents a cached certificate chain.
41 -type CertCacheEntry struct {
42 - CertPEM []byte `json:"cert_pem"`
43 - RootCAPEM []byte `json:"root_ca_pem"`
44 - FetchedAt int64 `json:"fetched_at"`
45 -}
46 -
47 -// ParseCertificatePEM parses a PEM-encoded certificate.
48 -func ParseCertificatePEM(pemData []byte) (*x509.Certificate, error) {
49 - block, _ := pem.Decode(pemData)
50 - if block == nil {
51 - return nil, errors.New("no PEM block found")
52 - }
53 - return x509.ParseCertificate(block.Bytes)
54 -}
55 -
56 -// LoadCertCache loads a certificate cache from file.
57 -func LoadCertCache(path string) (*CertCacheEntry, error) {
58 - if path == "" {
59 - return nil, nil
60 - }
61 - data, err := os.ReadFile(path)
62 - if err != nil {
63 - if os.IsNotExist(err) {
64 - return nil, nil
65 - }
66 - return nil, fmt.Errorf("read cert cache: %w", err)
67 - }
68 - var entry CertCacheEntry
69 - if err := json.Unmarshal(data, &entry); err != nil {
70 - return nil, fmt.Errorf("parse cert cache: %w", err)
71 - }
72 - return &entry, nil
73 -}
74 -
75 -// SaveCertCache saves a certificate cache to file.
76 -func SaveCertCache(path string, entry *CertCacheEntry) error {
77 - if path == "" || entry == nil {
78 - return nil
79 - }
80 -
81 - // Ensure parent directory exists
82 - dir := filepath.Dir(path)
83 - if dir != "" && dir != "." {
84 - if err := os.MkdirAll(dir, 0700); err != nil {
85 - return fmt.Errorf("create cert cache directory: %w", err)
86 - }
87 - }
88 -
89 - toSave := *entry
90 - toSave.FetchedAt = time.Now().Unix()
91 -
92 - data, err := json.Marshal(&toSave)
93 - if err != nil {
94 - return fmt.Errorf("marshal cert cache: %w", err)
95 - }
96 - if err := writeFileAtomic(path, data, 0o600); err != nil {
97 - return fmt.Errorf("write cert cache: %w", err)
98 - }
99 - return nil
100 -}
101 -
102 -// IsCertCacheFresh checks if a cached certificate is still fresh.
103 -// It checks both the TTL (time since fetch) and the certificate expiration.
104 -func IsCertCacheFresh(entry *CertCacheEntry, ttl time.Duration) bool {
105 - if entry == nil || len(entry.CertPEM) == 0 {
106 - return false
107 - }
108 -
109 - // Check TTL (how long since we fetched)
110 - fetchedAt := time.Unix(entry.FetchedAt, 0)
111 - if time.Since(fetchedAt) >= ttl {
112 - return false
113 - }
114 -
115 - // Check cert expiration (in case ACME renewed on relay side)
116 - cert, err := ParseCertificatePEM(entry.CertPEM)
117 - if err != nil {
118 - return false
119 - }
120 - timeUntilExpiry := time.Until(cert.NotAfter)
121 - if timeUntilExpiry < ExpiryThreshold {
122 - log.Debug().
123 - Time("not_after", cert.NotAfter).
124 - Dur("time_remaining", timeUntilExpiry).
125 - Msg("[certcache] Cached certificate is close to expiration, will refresh")
126 - return false
127 - }
128 -
129 - return true
130 -}
portal/acme/renew.go
+11
@@ -2,6 +2,8 @@ package acme
2
3 import (
4 "context"
5 + "crypto/x509"
6 + "encoding/pem"
7 "errors"
8 "fmt"
9 "os"
@@ -251,3 +253,12 @@ func (m *AcmeManager) doRenew(cfg provisionConfig) error {
253
254 return nil
255 }
256 +
257 +// ParseCertificatePEM parses a PEM-encoded certificate.
258 +func ParseCertificatePEM(pemData []byte) (*x509.Certificate, error) {
259 + block, _ := pem.Decode(pemData)
260 + if block == nil {
261 + return nil, errors.New("no PEM block found")
262 + }
263 + return x509.ParseCertificate(block.Bytes)
264 +}
sdk/client.go
-33
@@ -23,7 +23,6 @@ import (
23
24 "github.com/rs/zerolog/log"
25 "gosuda.org/portal/portal"
26 - "gosuda.org/portal/portal/acme"
26 )
27
28 var urlSafeNameRegex = regexp.MustCompile(`^[\p{L}\p{N}_-]+$`)
@@ -164,8 +163,6 @@ func (c *Client) Listen(name string, options ...MetadataOption) (net.Listener, e
163 keylessServerName,
164 c.config.TLSKeylessCertificatePEM,
165 c.config.TLSKeyless.RootCAPEM,
167 - c.config.CertCacheFile,
168 - acme.DefaultCacheTTL,
166 )
167 if err != nil {
168 return nil, fmt.Errorf("prepare keyless materials: %w", err)
@@ -355,8 +352,6 @@ func resolveKeylessMaterials(
352 keylessServerName string,
353 inlineCertPEM []byte,
354 inlineRootCAPEM []byte,
358 - cacheFile string,
359 - cacheTTL time.Duration,
355 ) ([]byte, []byte, error) {
356 certPEM := append([]byte(nil), inlineCertPEM...)
357 rootCAPEM := append([]byte(nil), inlineRootCAPEM...)
@@ -366,22 +361,6 @@ func resolveKeylessMaterials(
361 return certPEM, rootCAPEM, nil
362 }
363
369 - // Try loading from cache first
370 - if cacheEntry, err := acme.LoadCertCache(cacheFile); err != nil {
371 - log.Debug().Err(err).Msg("[SDK] Failed to load cert cache")
372 - } else if cacheEntry != nil && acme.IsCertCacheFresh(cacheEntry, cacheTTL) {
373 - log.Debug().Str("path", cacheFile).Msg("[SDK] Using cached certificate")
374 - if len(certPEM) == 0 && len(cacheEntry.CertPEM) > 0 {
375 - certPEM = cacheEntry.CertPEM
376 - }
377 - if len(rootCAPEM) == 0 && len(cacheEntry.RootCAPEM) > 0 {
378 - rootCAPEM = cacheEntry.RootCAPEM
379 - }
380 - if len(certPEM) > 0 && len(rootCAPEM) > 0 {
381 - return certPEM, rootCAPEM, nil
382 - }
383 - }
384 -
364 // Fetch from endpoint
365 chainFromEndpoint, err := fetchEndpointCertificateChain(ctx, keylessEndpoint, keylessServerName)
366 if err != nil && len(certPEM) == 0 {
@@ -405,18 +384,6 @@ func resolveKeylessMaterials(
384 rootCAPEM = append([]byte(nil), certPEM...)
385 }
386
408 - // Save to cache
409 - if cacheFile != "" {
410 - if err := acme.SaveCertCache(cacheFile, &acme.CertCacheEntry{
411 - CertPEM: certPEM,
412 - RootCAPEM: rootCAPEM,
413 - }); err != nil {
414 - log.Debug().Err(err).Msg("[SDK] Failed to save cert cache")
415 - } else {
416 - log.Debug().Str("path", cacheFile).Msg("[SDK] Saved certificate cache")
417 - }
418 - }
419 -
387 return certPEM, rootCAPEM, nil
388 }
389
sdk/types.go
-12
@@ -59,10 +59,6 @@ type ClientConfig struct {
59 // Optional certificate chain and remote signer config used by keyless mode.
60 TLSKeylessCertificatePEM []byte
61 TLSKeyless TLSKeylessConfig
62 -
63 - // CertCacheFile is the path to cache keyless certificates.
64 - // If set, certificates are cached to avoid re-fetching on every startup.
65 - CertCacheFile string
62 }
63
64 type ClientOption func(*ClientConfig)
@@ -152,14 +148,6 @@ func WithTLSKeylessDefaults() ClientOption {
148 }
149 }
150
155 -// WithCertCacheFile sets the path for caching keyless certificates.
156 -// Cached certificates are reused until they expire (default 24 hours).
157 -func WithCertCacheFile(path string) ClientOption {
158 - return func(c *ClientConfig) {
159 - c.CertCacheFile = path
160 - }
161 -}
162 -
151 // MetadataOption configures Metadata
152 type MetadataOption func(*portal.Metadata)
153