Skip to content

Commit 260e63f

Browse files
feat(autotls): add support for custom DNS resolver and include Pebble… (#572)
* feat: add new flags * fix(k8s): ensure p2p-wss service port is created when configured * chore: add comments to local.yaml * fix(config): update autotls-registration-endpoint * feat(autotls): add autotls support with pebble and p2p-forge (#559) * feat(autotls): add auto tls support in init containers and configuration options * fix(k8s): fix EnvVar handling by adding hasValues method and improving toK8S conversion * feat(config): add p2p-wss support and update autotls registration endpoint in configuration files * chore(config): remove unneeded p2p-wss settings * feat(autotls): implement autotls check functionality (#560) * feat(autotls): implement autotls check functionality * feat(autotls): enhance WSS connectivity checks and update configuration * feat(autotls): add autotls check in config.yaml * feat(config): add WSS configuration for local setup * fix(autotls): ensure context cancellation is handled correctly during WSS connection tests * fix(autotls): update WSS connectivity test to disconnect everything before connecting * fix(autotls): add WSS group option and enhance error handling for WSS connectivity * feat(autotls): add certificate renewal testing options and enhance configuration * refactor(autotls): simplify certificate renewal options and update related configurations * fix(autotls): increase certificate renewal wait time to 500 seconds for improved testing * fix(autotls): extend timeout for certificate renewal to 15 minutes * feat(autotls): add ultralight group support and connectivity testing * feat(config): update local.yaml for ultralight configuration and enhance autotls checks * fix(autotls): re-enable certificate renewal test * feat(config): add local-dns-autotls and local-light-autotls configura… (#562) * feat(config): add local-dns-autotls and local-light-autotls configurations ons. * chore(config): comment out ultralight configuration * fix(config): update autotls configs * fix(autotls): simplify config * fix: use test cert from constant * fix(autotls): handle context cancellation during sleep intervals in c… (#566) * fix(autotls): handle context cancellation during sleep intervals in checks * feat(autotls): add UnderlayPollInterval option and enhance WSS underlay checks * refactor(config): reorder struct fields for clarity in check options * refactor(autotls): simplify options structure and improve WSS underlay checks * feat(config): add light node configuration for autotls support in local.yaml * fix: git conflicts * chore: fix lint issue * feat(autotls): add support for custom DNS resolver and include Pebble CA certificate * lint forge test file * feat(autotls): enhance Pebble CA certificate fetching and direct DNS queries * chore: update forge DNS address in local configuration * chore: update forgeDNSAddr in check.go * chore: enhance DNS resolution handling with custom DNS server support * chore: remove unnecessary changes related to pebble * chore: update package imports * chore: rename ForgeDNSAddr * feat(autotls): add Forge TLS host address and Pebble management URL support * fix(autotls): increase certificate renewal wait time to accommodate expiry and check interval * fix(autotls): extend timeout for certificate renewal process * fix(autotls): adjust certificate renewal logic and improve logging for clarity * fix(autotls): refine certificate renewal process with enhanced retry logic and improved snapshot handling * refator: rm test forge * feat(autotls): introduce support for multiple AutoTLS groups and add P2P WSS node port configuration * refactor(autotls): consolidate HTTP client configuration for Pebble management API calls * fix: lint check.go * refactor(autotls): encapsulate Pebble management API interactions in a dedicated service * fix(autotls): enhance logging for certificate snapshot dialing errors * refactor(autotls): move certificate renewal logic to a dedicated function and improve structure * docs(autotls): add README for AutoTLS check detailing functionality, configuration, and package layout * docs(autotls): add sequence diagram to README for better visualization of AutoTLS check process * chore(config): update local.yaml with new addresses for forge DNS and Pebble management URL * chore(config): add local usage notes for forge DNS and Pebble management URL in local.yaml * fix: set timeout for insecure Pebble HTTP client * chore(config): update local.yaml with valid values for CI * chore(deps): update go.mod to include new dependencies and indirect packages --------- Co-authored-by: Ljubisa Gacevic <ljubisa.rs@gmail.com>
1 parent 1f92f21 commit 260e63f

16 files changed

Lines changed: 559 additions & 134 deletions

File tree

config/local.yaml

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,14 @@ clusters:
6565
count: 3
6666
mode: node
6767
bee-autotls:
68+
bee-config: bee-local-autotls
69+
config: local-autotls
70+
count: 1
71+
mode: node
72+
bee-autotls-extra:
6873
bee-config: bee-local-autotls
6974
config: local
70-
count: 2
75+
count: 1
7176
mode: node
7277
light:
7378
bee-config: bee-local-light-autotls
@@ -132,6 +137,9 @@ node-groups:
132137
update-strategy: "RollingUpdate"
133138
local-dns:
134139
_inherit: "local"
140+
local-autotls:
141+
_inherit: "local"
142+
p2p-wss-node-port: 31635
135143
local-gc:
136144
_inherit: "local"
137145
local-light:
@@ -447,8 +455,14 @@ checks:
447455
postage-label: test-label
448456
type: feed
449457
ci-autotls:
450-
timeout: 15m
458+
timeout: 25m
451459
type: autotls
452460
options:
453461
ultra-light-group: ultra-light
454-
autotls-group: bee-autotls
462+
autotls-groups:
463+
- bee-autotls
464+
- bee-autotls-extra
465+
forge-dns-address: "127.0.0.1:30053" # When running inside cluster, use p2p-forge.local.svc.cluster.local:53
466+
forge-tls-host-address: "" # When running locally, use 127.0.0.1:31635
467+
pebble-mgmt-url: "https://127.0.0.1:31500/roots/0" # When running inside cluster, use https://pebble.local.svc.cluster.local:15000/roots/0
468+

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require (
1818
github.com/gorilla/websocket v1.5.3
1919
github.com/ipshipyard/p2p-forge v0.7.0
2020
github.com/libp2p/go-libp2p v0.46.0
21+
github.com/miekg/dns v1.1.66
2122
github.com/multiformats/go-multiaddr v0.16.1
2223
github.com/multiformats/go-multibase v0.2.0
2324
github.com/opentracing/opentracing-go v1.2.0
@@ -107,7 +108,6 @@ require (
107108
github.com/mailru/easyjson v0.7.7 // indirect
108109
github.com/mattn/go-isatty v0.0.20 // indirect
109110
github.com/mholt/acmez/v3 v3.0.0 // indirect
110-
github.com/miekg/dns v1.1.66 // indirect
111111
github.com/minio/sha256-simd v1.0.1 // indirect
112112
github.com/mitchellh/mapstructure v1.5.0 // indirect
113113
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect

pkg/cert/pebble.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package cert
2+
3+
// PebbleCertificate is the root CA certificate for Pebble (Let's Encrypt test server).
4+
// Source: https://github.com/letsencrypt/pebble/blob/main/test/certs/pebble.minica.pem
5+
const PebbleCertificate = `-----BEGIN CERTIFICATE-----
6+
MIIDPzCCAiegAwIBAgIIU0Xm9UFdQxUwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE
7+
AxMVbWluaWNhIHJvb3QgY2EgNTM0NWU2MCAXDTI1MDkwMzIzNDAwNVoYDzIxMjUw
8+
OTAzMjM0MDA1WjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA1MzQ1ZTYwggEi
9+
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5WgZNoVJandj43kkLyU50vzCZ
10+
alozvdRo3OFiKoDtmqKPNWRNO2hC9AUNxTDJco51Yc42u/WV3fPbbhSznTiOOVtn
11+
Ajm6iq4I5nZYltGGZetGDOQWr78y2gWY+SG078MuOO2hyDIiKtVc3xiXYA+8Hluu
12+
9F8KbqSS1h55yxZ9b87eKR+B0zu2ahzBCIHKmKWgc6N13l7aDxxY3D6uq8gtJRU0
13+
toumyLbdzGcupVvjbjDP11nl07RESDWBLG1/g3ktJvqIa4BWgU2HMh4rND6y8OD3
14+
Hy3H8MY6CElL+MOCbFJjWqhtOxeFyZZV9q3kYnk9CAuQJKMEGuN4GU6tzhW1AgMB
15+
AAGjezB5MA4GA1UdDwEB/wQEAwIChDATBgNVHSUEDDAKBggrBgEFBQcDATASBgNV
16+
HRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSu8RGpErgYUoYnQuwCq+/ggTiEjDAf
17+
BgNVHSMEGDAWgBSu8RGpErgYUoYnQuwCq+/ggTiEjDANBgkqhkiG9w0BAQsFAAOC
18+
AQEAXDVYov1+f6EL7S41LhYQkEX/GyNNzsEvqxE9U0+3Iri5JfkcNOiA9O9L6Z+Y
19+
bqcsXV93s3vi4r4WSWuc//wHyJYrVe5+tK4nlFpbJOvfBUtnoBDyKNxXzZCxFJVh
20+
f9uc8UejRfQMFbDbhWY/x83y9BDufJHHq32OjCIN7gp2UR8rnfYvlz7Zg4qkJBsn
21+
DG4dwd+pRTCFWJOVIG0JoNhK3ZmE7oJ1N4H38XkZ31NPcMksKxpsLLIS9+mosZtg
22+
4olL7tMPJklx5ZaeMFaKRDq4Gdxkbw4+O4vRgNm3Z8AXWKknOdfgdpqLUPPhRcP4
23+
v1lhy71EhBuXXwRQJry0lTdF+w==
24+
-----END CERTIFICATE-----`

pkg/check/autotls/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# autotls check
2+
3+
Beekeeper check that validates **p2p-forge** style AutoTLS setup: WSS underlays, forge hostnames, DNS, TLS on forge endpoints, connectivity, and (when possible) **certificate renewal** by comparing cert serials over time.
4+
5+
## What it runs (order)
6+
7+
1. Load API clients for nodes in the configured AutoTLS node groups.
8+
2. **WSS underlays** — confirm nodes expose WebSocket secure underlays (optionally skip a group such as ultra-light).
9+
3. **Forge domain + CA** — read `AutoTLSDomain` (and Pebble CA handling) from the first matching node config.
10+
4. **Forge address format** — parse WSS multiaddrs and check forge hostname / peer id consistency.
11+
5. **DNS** — resolve forge hostnames when `forge-dns-address` is set.
12+
6. **TLS** — dial forge endpoints and verify certificates (SANs, retries for expired certs).
13+
7. **WSS connectivity** — disconnect/reconnect between nodes over WSS underlays.
14+
8. **Ultra-light** (optional) — same connectivity from ultra-light nodes if configured.
15+
9. **Renewal** — snapshot leaf cert serials, wait until near expiry (or trigger dials if already expired), snapshot again, compare serials; then run WSS connectivity again.
16+
17+
If any step fails, the check fails.
18+
19+
## Config (beekeeper)
20+
21+
Check type: `autotls`. Options map to `Options` in `autotls.go`:
22+
23+
| YAML key | Purpose |
24+
|----------|---------|
25+
| `autotls-groups` | Node groups that run AutoTLS (default in code: `bee-autotls`). |
26+
| `ultra-light-group` | Group name for nodes without listen addrs; excluded from WSS underlay collection, used for ultra-light connectivity test. Default: `ultra-light`. Set empty to skip ultra-light tests. |
27+
| `forge-dns-address` | Resolver host:port used to verify DNS resolution of forge hostnames. |
28+
| `forge-tls-host-address` | Optional `host:port` to dial the **first** sorted node’s forge TLS check from this host (e.g. in-cluster DNS). Other nodes still use IP:port from the multiaddr. |
29+
| `pebble-mgmt-url` | Override Pebble **management** URL for fetching the live root CA PEM (see below). |
30+
31+
Defaults: `autotls.NewDefaultOptions()` in `autotls.go`.
32+
33+
## Pebble (local ACME)
34+
35+
When a node’s `AutoTLSCAEndpoint` contains `pebble`, the check fetches the **current** root CA from Pebble’s management API (Pebble rotates its CA on restart). The ACME directory URL in config is turned into a management URL with `pebbleMgmtURL()` (ACME port `14000` → management `15000`, path `/roots/0`). `pebble-mgmt-url` overrides that derived URL.
36+
37+
The HTTP client uses **TLS insecure skip verify** only for that management HTTPS call (self-signed Pebble). Implementation: `internal/service.go` (`Pebble.FetchRootCA`), client wired in `autotls.go`.
38+
39+
## Package layout
40+
41+
| File | Role |
42+
|------|------|
43+
| `autotls.go` | `Check`, `Run`, options, WSS underlay polling, connectivity tests, `forgeConfig`, Pebble URL helper. |
44+
| `forge.go` | Forge multiaddr parsing, DNS/TLS verification helpers, `getCertSnapshots`, `triggerRenewalConnections`, `certSnapshot`. |
45+
| `renewal.go` | Renewal orchestration: wait/compare/retry, `compareCertRenewals` helpers. |
46+
| `internal/service.go` | Small HTTP client wrapper for Pebble management `GET` (root CA PEM). |
47+
48+
Renewal **orchestration** lives in `renewal.go`; TLS dialing and address selection for snapshots and renewal triggers stay in `forge.go` next to other forge TLS code.
49+
50+
## Sequence diagram
51+
52+
![AutoTLS check sequence diagram](autotls-sequence-diagram.png)
791 KB
Loading

pkg/check/autotls/autotls.go

Lines changed: 54 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,32 @@ package autotls
22

33
import (
44
"context"
5+
"crypto/tls"
56
"fmt"
7+
"net/http"
68
"strings"
79
"time"
810

911
"github.com/ethersphere/beekeeper/pkg/bee"
1012
"github.com/ethersphere/beekeeper/pkg/beekeeper"
13+
"github.com/ethersphere/beekeeper/pkg/cert"
14+
"github.com/ethersphere/beekeeper/pkg/check/autotls/internal"
1115
"github.com/ethersphere/beekeeper/pkg/logging"
1216
"github.com/ethersphere/beekeeper/pkg/orchestration"
13-
"github.com/ethersphere/beekeeper/pkg/orchestration/k8s"
1417
ma "github.com/multiformats/go-multiaddr"
1518
)
1619

1720
type Options struct {
18-
AutoTLSGroup string
19-
UltraLightGroup string
21+
AutoTLSGroups []string
22+
UltraLightGroup string
23+
ForgeDNSAddress string
24+
ForgeTLSHostAddress string
25+
PebbleMgmtURL string
2026
}
2127

2228
func NewDefaultOptions() Options {
2329
return Options{
24-
AutoTLSGroup: "bee-autotls",
30+
AutoTLSGroups: []string{"bee-autotls"},
2531
UltraLightGroup: "ultra-light",
2632
}
2733
}
@@ -33,6 +39,13 @@ const (
3339

3440
var _ beekeeper.Action = (*Check)(nil)
3541

42+
var insecurePebbleHTTPClient = &http.Client{
43+
Timeout: connectTimeout,
44+
Transport: &http.Transport{
45+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
46+
},
47+
}
48+
3649
type Check struct {
3750
logger logging.Logger
3851
}
@@ -56,20 +69,19 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any
5669
return fmt.Errorf("get node clients: %w", err)
5770
}
5871

59-
autoTLSClients := orchestration.ClientMap(clients).FilterByNodeGroups([]string{o.AutoTLSGroup})
72+
autoTLSClients := orchestration.ClientMap(clients).FilterByNodeGroups(o.AutoTLSGroups)
6073
if len(autoTLSClients) == 0 {
61-
return fmt.Errorf("no nodes found in AutoTLS group %q", o.AutoTLSGroup)
74+
return fmt.Errorf("no nodes found in AutoTLS groups %v", o.AutoTLSGroups)
6275
}
6376

64-
c.logger.Infof("found %d nodes in AutoTLS group %q", len(autoTLSClients), o.AutoTLSGroup)
77+
c.logger.Infof("found %d nodes in AutoTLS groups %v", len(autoTLSClients), o.AutoTLSGroups)
6578

6679
wssNodes, err := c.verifyWSSUnderlays(ctx, autoTLSClients, o.UltraLightGroup)
6780
if err != nil {
6881
return fmt.Errorf("verify WSS underlays: %w", err)
6982
}
7083

71-
// Extract forge config from the first autotls node's bee config.
72-
forgeDomain, caCertPEM := c.forgeConfig(cluster, autoTLSClients)
84+
forgeDomain, caCertPEM := c.forgeConfig(ctx, cluster, autoTLSClients, o.PebbleMgmtURL)
7385
if forgeDomain == "" {
7486
return fmt.Errorf("could not determine forge domain from node config")
7587
}
@@ -79,11 +91,11 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any
7991
return fmt.Errorf("forge address validation: %w", err)
8092
}
8193

82-
if err := c.verifyDNSResolution(ctx, forgeNodes); err != nil {
94+
if err := c.verifyDNSResolution(ctx, forgeNodes, o.ForgeDNSAddress); err != nil {
8395
return fmt.Errorf("DNS resolution verification: %w", err)
8496
}
8597

86-
if err := c.verifyTLSCertificate(ctx, forgeNodes, caCertPEM); err != nil {
98+
if err := c.verifyTLSCertificate(ctx, forgeNodes, caCertPEM, o.ForgeTLSHostAddress); err != nil {
8799
return fmt.Errorf("TLS certificate verification: %w", err)
88100
}
89101

@@ -97,7 +109,7 @@ func (c *Check) Run(ctx context.Context, cluster orchestration.Cluster, opts any
97109
}
98110
}
99111

100-
if err := c.testCertificateRenewal(ctx, clients, wssNodes, forgeNodes, caCertPEM, connectTimeout); err != nil {
112+
if err := c.testCertificateRenewal(ctx, clients, wssNodes, forgeNodes, caCertPEM, o.ForgeTLSHostAddress, connectTimeout); err != nil {
101113
return fmt.Errorf("certificate renewal test: %w", err)
102114
}
103115

@@ -276,66 +288,14 @@ func (c *Check) testConnectivity(ctx context.Context, sourceClient *bee.Client,
276288
return nil
277289
}
278290

279-
func (c *Check) testCertificateRenewal(ctx context.Context, clients map[string]*bee.Client, wssNodes map[string][]string, forgeNodes map[string][]*forgeUnderlayInfo, caCertPEM string, connectTimeout time.Duration) error {
280-
const renewalWaitTime = 350 * time.Second // This is configured in beelocal setup (we set certificate to expire in 300 seconds)
281-
282-
// Snapshot certificate serial numbers before waiting.
283-
preSerials := c.getCertSerials(ctx, forgeNodes, caCertPEM)
284-
if len(preSerials) > 0 {
285-
c.logger.Infof("captured %d certificate serial(s) before renewal wait", len(preSerials))
286-
} else {
287-
c.logger.Warning("no TLS endpoints reachable, will fall back to connectivity-only renewal check")
288-
}
289-
290-
c.logger.Infof("testing certificate renewal: waiting %v for certificates to expire and renew", renewalWaitTime)
291-
292-
select {
293-
case <-ctx.Done():
294-
return ctx.Err()
295-
case <-time.After(renewalWaitTime):
296-
}
297-
298-
c.logger.Info("wait complete, verifying certificates were renewed")
299-
300-
// Verify serial numbers changed (proves new certs were issued).
301-
if len(preSerials) > 0 {
302-
postSerials := c.getCertSerials(ctx, forgeNodes, caCertPEM)
303-
var renewed, unchanged int
304-
for key, preSN := range preSerials {
305-
postSN, ok := postSerials[key]
306-
if !ok {
307-
c.logger.Warningf("%s: endpoint became unreachable after wait", key)
308-
continue
309-
}
310-
if preSN == postSN {
311-
unchanged++
312-
c.logger.Warningf("%s: certificate serial unchanged (%s), renewal may not have occurred", key, preSN)
313-
} else {
314-
renewed++
315-
c.logger.Infof("%s: certificate renewed (serial %s -> %s)", key, preSN, postSN)
316-
}
317-
}
318-
if unchanged > 0 && renewed == 0 {
319-
return fmt.Errorf("no certificates were renewed: %d/%d serials unchanged", unchanged, len(preSerials))
320-
}
321-
c.logger.Infof("certificate renewal verified: %d renewed, %d unchanged", renewed, unchanged)
322-
}
323-
324-
// Also verify WSS connectivity still works with the new certificates.
325-
if err := c.testWSSConnectivity(ctx, clients, wssNodes, connectTimeout); err != nil {
326-
return fmt.Errorf("post-renewal connectivity test failed (certificates may not have been renewed): %w", err)
327-
}
328-
329-
c.logger.Info("certificate renewal test passed")
330-
return nil
331-
}
332-
333-
// forgeConfig extracts the forge domain and appropriate CA certificate from the
334-
// first autotls node's bee configuration. If the CA endpoint indicates pebble
335-
// (test environment), the embedded pebble CA cert is returned. Otherwise, an
336-
// empty string is returned so the system root pool is used.
337-
func (c *Check) forgeConfig(cluster orchestration.Cluster, autoTLSClients orchestration.ClientList) (forgeDomain, caCertPEM string) {
291+
// forgeConfig extracts the forge domain and CA certificate from the first autotls
292+
// node's bee configuration. When Pebble is detected, the live root CA is fetched
293+
// from Pebble's management API (since Pebble generates a fresh CA on each start).
294+
// Falls back to the static embedded cert if the fetch fails.
295+
func (c *Check) forgeConfig(ctx context.Context, cluster orchestration.Cluster, autoTLSClients orchestration.ClientList, pebbleMgmtURLOverride string) (forgeDomain, caCertPEM string) {
338296
nodes := cluster.Nodes()
297+
pebbleSvc := internal.NewPebble(insecurePebbleHTTPClient)
298+
339299
for _, client := range autoTLSClients {
340300
node, ok := nodes[client.Name()]
341301
if !ok || node.Config() == nil {
@@ -344,9 +304,31 @@ func (c *Check) forgeConfig(cluster orchestration.Cluster, autoTLSClients orches
344304
cfg := node.Config()
345305
forgeDomain = cfg.AutoTLSDomain
346306
if strings.Contains(cfg.AutoTLSCAEndpoint, "pebble") {
347-
caCertPEM = k8s.PebbleCertificate
307+
mgmtURL := pebbleMgmtURL(cfg.AutoTLSCAEndpoint)
308+
if pebbleMgmtURLOverride != "" {
309+
mgmtURL = pebbleMgmtURLOverride
310+
}
311+
liveCert, err := pebbleSvc.FetchRootCA(ctx, mgmtURL)
312+
if err != nil {
313+
c.logger.Warningf("failed to fetch live Pebble CA from %s, falling back to static cert: %v", mgmtURL, err)
314+
caCertPEM = cert.PebbleCertificate
315+
} else {
316+
c.logger.Infof("fetched live Pebble CA from %s", mgmtURL)
317+
caCertPEM = liveCert
318+
}
348319
}
349320
return forgeDomain, caCertPEM
350321
}
351322
return "", ""
352323
}
324+
325+
// pebbleMgmtURL derives the Pebble management API URL from the ACME directory endpoint.
326+
// E.g. "https://pebble:14000/dir" -> "https://pebble:15000/roots/0"
327+
func pebbleMgmtURL(acmeEndpoint string) string {
328+
base := acmeEndpoint
329+
if i := strings.LastIndex(base, "/"); i > 0 {
330+
base = base[:i]
331+
}
332+
base = strings.Replace(base, ":14000", ":15000", 1)
333+
return base + "/roots/0"
334+
}

0 commit comments

Comments
 (0)