Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions cmd/onecli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,27 +135,57 @@ func (c *RunCmd) Run(out *output.Writer) error {
return nil
}

// writeGatewayCACert writes the gateway CA PEM to ~/.onecli/gateway-ca.pem.
// Returns the path on success. Skips the write if on-disk content already matches.
func writeGatewayCACert(pem string) (string, error) {
// writeGatewayCACert writes a combined CA bundle (system CAs + gateway CA)
// to ~/.onecli/ca-bundle.pem. Env vars like SSL_CERT_FILE REPLACE the
// default trust store, so the bundle must include system root certificates
// alongside the gateway CA.
func writeGatewayCACert(gatewayPEM string) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolving home dir: %w", err)
}
caPath := filepath.Join(home, ".onecli", "gateway-ca.pem")
caPath := filepath.Join(home, ".onecli", "ca-bundle.pem")
if err := os.MkdirAll(filepath.Dir(caPath), 0o700); err != nil {
return "", fmt.Errorf("creating CA dir: %w", err)
}

var buf bytes.Buffer
if systemCAs, err := readSystemCAs(); err == nil {
buf.Write(systemCAs)
if len(systemCAs) > 0 && systemCAs[len(systemCAs)-1] != '\n' {
buf.WriteByte('\n')
}
}
buf.WriteString(gatewayPEM)

combined := buf.Bytes()
existing, err := os.ReadFile(caPath)
if err == nil && bytes.Equal(existing, []byte(pem)) {
if err == nil && bytes.Equal(existing, combined) {
return caPath, nil
}
if err := os.WriteFile(caPath, []byte(pem), 0o600); err != nil {
return "", fmt.Errorf("writing CA cert: %w", err)
if err := os.WriteFile(caPath, combined, 0o600); err != nil {
return "", fmt.Errorf("writing CA bundle: %w", err)
}
return caPath, nil
}

var systemCAPaths = []string{
"/etc/ssl/cert.pem", // macOS
"/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu
"/etc/pki/tls/certs/ca-bundle.crt", // RHEL/Fedora/CentOS
"/etc/ssl/ca-bundle.pem", // SUSE
}

func readSystemCAs() ([]byte, error) {
for _, p := range systemCAPaths {
data, err := os.ReadFile(p)
if err == nil && len(data) > 0 {
return data, nil
}
}
return nil, fmt.Errorf("no system CA bundle found")
}

// caTrustKeys are env vars we inject locally for CA trust. These aren't in
// the server response but may exist in the parent env and need stripping.
var caTrustKeys = []string{
Expand Down
32 changes: 27 additions & 5 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package api
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)

Expand All @@ -21,14 +24,33 @@ type Client struct {
// New creates an API client.
func New(baseURL, apiKey string) *Client {
return &Client{
baseURL: baseURL,
apiKey: apiKey,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
baseURL: baseURL,
apiKey: apiKey,
httpClient: buildHTTPClient(),
}
}

func buildHTTPClient() *http.Client {
client := &http.Client{Timeout: 30 * time.Second}
f := os.Getenv("SSL_CERT_FILE")
if f == "" {
return client
}
data, err := os.ReadFile(f)
if err != nil {
return client
}
pool, err := x509.SystemCertPool()
if err != nil {
pool = x509.NewCertPool()
}
pool.AppendCertsFromPEM(data)
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: pool},
}
return client
}

// withProjectQuery appends a projectId query param to path when projectID is non-empty.
func withProjectQuery(basePath, projectID string) string {
if projectID == "" {
Expand Down
Loading