Skip to content

Commit d36e9c3

Browse files
committed
release(agent): prepare v1.0.7
1 parent 6d8f179 commit d36e9c3

6 files changed

Lines changed: 81 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ Formatting rules:
1515

1616
## [Unreleased]
1717

18+
## [1.0.7] - 2026-04-08
19+
20+
### Added
21+
22+
- Added support for custom API TLS trust roots through `api_tls_ca_file` in config files and `NODERAX_API_TLS_CA_FILE` / `API_TLS_CA_FILE` environment overrides.
23+
24+
### Changed
25+
26+
- Changed API client construction to initialize with system CA roots plus optional custom CA bundle loading, and enforced TLS `minVersion` at TLS 1.2 for outbound API requests.
27+
- Changed enrollment and managed update code paths to use error-returning API client initialization so TLS CA configuration issues are surfaced before network operations start.
28+
29+
### Fixed
30+
31+
- Fixed startup, install, bootstrap, and managed update flows to fail fast with explicit `configure API client` errors when API TLS CA files are unreadable or invalid.
32+
1833
## [1.0.6] - 2026-04-05
1934

2035
### Added

cmd/agent/main.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ func main() {
5353
log := logger.New(cfg.LogLevel)
5454
log.Info("starting noderax agent", "version", version, "commit", commit, "build_date", buildDate)
5555

56-
client := api.NewClient(cfg.APIURL, cfg.RequestTimeout)
56+
client, err := api.NewClient(cfg.APIURL, cfg.RequestTimeout, cfg.APITLSCAFile)
57+
if err != nil {
58+
log.Error("configure API client", "error", err)
59+
os.Exit(1)
60+
}
5761
if cfg.AgentToken != "" {
5862
client.SetAgentToken(cfg.AgentToken)
5963
}

internal/agentctl/commands.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,10 @@ func (c CLI) Install(ctx context.Context, args []string) error {
281281
return err
282282
}
283283
} else {
284-
client := api.NewClient(cfg.APIURL, cfg.RequestTimeout)
284+
client, err := api.NewClient(cfg.APIURL, cfg.RequestTimeout, cfg.APITLSCAFile)
285+
if err != nil {
286+
return fmt.Errorf("configure API client: %w", err)
287+
}
285288
if err := agent.RunInteractiveEnrollment(ctx, cfg, client, c.Logger, c.Version, c.stdinOrDefault(), c.stdoutOrDefault()); err != nil {
286289
return err
287290
}
@@ -334,7 +337,10 @@ func (c CLI) Bootstrap(ctx context.Context, args []string) error {
334337
}
335338
}
336339

337-
client := api.NewClient(cfg.APIURL, cfg.RequestTimeout)
340+
client, err := api.NewClient(cfg.APIURL, cfg.RequestTimeout, cfg.APITLSCAFile)
341+
if err != nil {
342+
return fmt.Errorf("configure API client: %w", err)
343+
}
338344
_, err = agent.RunBootstrapEnrollment(
339345
ctx,
340346
cfg,
@@ -1796,8 +1802,11 @@ func promptValue(reader *bufio.Reader, writer io.Writer, label, defaultValue str
17961802

17971803
func (c CLI) bootstrapManagedInstall(ctx context.Context, spec platformSpec, cfg config.Config) error {
17981804
if strings.TrimSpace(spec.ServiceUser) == "" {
1799-
client := api.NewClient(cfg.APIURL, cfg.RequestTimeout)
1800-
_, err := agent.RunBootstrapEnrollment(
1805+
client, err := api.NewClient(cfg.APIURL, cfg.RequestTimeout, cfg.APITLSCAFile)
1806+
if err != nil {
1807+
return fmt.Errorf("configure API client: %w", err)
1808+
}
1809+
_, err = agent.RunBootstrapEnrollment(
18011810
ctx,
18021811
cfg,
18031812
client,

internal/agentctl/update.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,10 @@ func (c CLI) applyManagedUpdate(
238238
return fmt.Errorf("managed agent identity is missing; self-update requires a registered node id and agent token")
239239
}
240240

241-
client := api.NewClient(cfg.APIURL, cfg.RequestTimeout)
241+
client, err := api.NewClient(cfg.APIURL, cfg.RequestTimeout, cfg.APITLSCAFile)
242+
if err != nil {
243+
return fmt.Errorf("configure API client: %w", err)
244+
}
242245
client.SetAgentNodeID(cfg.NodeID)
243246
client.SetAgentToken(cfg.AgentToken)
244247

@@ -386,7 +389,10 @@ func (c CLI) reportManagedUpdateProgress(
386389
return fmt.Errorf("managed agent identity is missing")
387390
}
388391

389-
client := api.NewClient(cfg.APIURL, cfg.RequestTimeout)
392+
client, err := api.NewClient(cfg.APIURL, cfg.RequestTimeout, cfg.APITLSCAFile)
393+
if err != nil {
394+
return fmt.Errorf("configure API client: %w", err)
395+
}
390396
client.SetAgentNodeID(cfg.NodeID)
391397
client.SetAgentToken(cfg.AgentToken)
392398

internal/api/client.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package api
22

33
import (
44
"context"
5+
"crypto/tls"
6+
"crypto/x509"
57
"fmt"
8+
"os"
69
"net/url"
710
"strings"
811
"time"
@@ -38,7 +41,7 @@ func (e *RequestError) Error() string {
3841
return fmt.Sprintf("%s %s: status=%d message=%s", strings.ToUpper(e.Method), e.Path, e.StatusCode, message)
3942
}
4043

41-
func NewClient(baseURL string, timeout time.Duration) *Client {
44+
func NewClient(baseURL string, timeout time.Duration, tlsCAFile string) (*Client, error) {
4245
httpClient := resty.New().
4346
SetBaseURL(strings.TrimRight(baseURL, "/")).
4447
SetTimeout(timeout).
@@ -55,7 +58,27 @@ func NewClient(baseURL string, timeout time.Duration) *Client {
5558
return response.StatusCode() == 429 || response.StatusCode() >= 500
5659
})
5760

58-
return &Client{http: httpClient}
61+
rootCAs, err := x509.SystemCertPool()
62+
if err != nil || rootCAs == nil {
63+
rootCAs = x509.NewCertPool()
64+
}
65+
66+
if strings.TrimSpace(tlsCAFile) != "" {
67+
pemBytes, readErr := os.ReadFile(strings.TrimSpace(tlsCAFile))
68+
if readErr != nil {
69+
return nil, fmt.Errorf("read API TLS CA file %s: %w", strings.TrimSpace(tlsCAFile), readErr)
70+
}
71+
if ok := rootCAs.AppendCertsFromPEM(pemBytes); !ok {
72+
return nil, fmt.Errorf("parse API TLS CA file %s: no certificates found", strings.TrimSpace(tlsCAFile))
73+
}
74+
}
75+
76+
httpClient.SetTLSClientConfig(&tls.Config{
77+
MinVersion: tls.VersionTLS12,
78+
RootCAs: rootCAs,
79+
})
80+
81+
return &Client{http: httpClient}, nil
5982
}
6083

6184
func (c *Client) SetAgentToken(token string) {

internal/config/config.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929

3030
type Config struct {
3131
APIURL string
32+
APITLSCAFile string
3233
EnrollmentToken string
3334
NodeID string
3435
AgentToken string
@@ -51,6 +52,7 @@ type Config struct {
5152

5253
type fileConfig struct {
5354
APIURL string `json:"api_url"`
55+
APITLSCAFile string `json:"api_tls_ca_file,omitempty"`
5456
EnrollmentToken string `json:"enrollment_token"`
5557
NodeID string `json:"node_id"`
5658
AgentToken string `json:"agent_token"`
@@ -138,6 +140,7 @@ func SaveFile(path string, cfg Config) error {
138140
realtimeBackoffJitter := cfg.RealtimeBackoffJitter
139141
raw := fileConfig{
140142
APIURL: cfg.APIURL,
143+
APITLSCAFile: cfg.APITLSCAFile,
141144
EnrollmentToken: cfg.EnrollmentToken,
142145
NodeID: cfg.NodeID,
143146
AgentToken: cfg.AgentToken,
@@ -196,6 +199,11 @@ func (c Config) Validate() error {
196199
if parsedURL.Host == "" {
197200
return fmt.Errorf("API_URL must include a host, got %q", c.APIURL)
198201
}
202+
if strings.TrimSpace(c.APITLSCAFile) != "" {
203+
if _, err := os.Stat(c.APITLSCAFile); err != nil {
204+
return fmt.Errorf("API_TLS_CA_FILE is invalid: %w", err)
205+
}
206+
}
199207
if c.HeartbeatInterval <= 0 {
200208
return fmt.Errorf("HEARTBEAT_INTERVAL must be greater than zero")
201209
}
@@ -281,6 +289,9 @@ func mergeConfigFile(cfg *Config, path string) error {
281289
if raw.APIURL != "" {
282290
cfg.APIURL = strings.TrimSpace(raw.APIURL)
283291
}
292+
if raw.APITLSCAFile != "" {
293+
cfg.APITLSCAFile = filepath.Clean(strings.TrimSpace(raw.APITLSCAFile))
294+
}
284295
if raw.EnrollmentToken != "" {
285296
cfg.EnrollmentToken = raw.EnrollmentToken
286297
}
@@ -339,6 +350,7 @@ func mergeConfigFile(cfg *Config, path string) error {
339350

340351
func mergeEnv(cfg *Config) error {
341352
overrideStringAny(&cfg.APIURL, "NODERAX_API_URL", "API_URL")
353+
overrideStringAny(&cfg.APITLSCAFile, "NODERAX_API_TLS_CA_FILE", "API_TLS_CA_FILE")
342354
overrideString(&cfg.EnrollmentToken, "ENROLLMENT_TOKEN")
343355
overrideString(&cfg.NodeID, "NODE_ID")
344356
overrideString(&cfg.AgentToken, "AGENT_TOKEN")
@@ -381,6 +393,9 @@ func mergeEnv(cfg *Config) error {
381393
if cfg.StateFile != "" {
382394
cfg.StateFile = filepath.Clean(cfg.StateFile)
383395
}
396+
if cfg.APITLSCAFile != "" {
397+
cfg.APITLSCAFile = filepath.Clean(cfg.APITLSCAFile)
398+
}
384399
if cfg.RealtimeNamespace != "" && !strings.HasPrefix(cfg.RealtimeNamespace, "/") {
385400
cfg.RealtimeNamespace = "/" + cfg.RealtimeNamespace
386401
}

0 commit comments

Comments
 (0)