Skip to content
Draft
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
15 changes: 12 additions & 3 deletions aks-node-controller/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,18 @@ recursively deleting directories or removing installed system-trust certificates
`AKS_CLOUD_LOG_CERTIFICATES=true` enables public-certificate-only diagnostics;
parsing for logging does not gate installation.

These are internal capabilities only. No certificate-retrieval flow, standalone
refresh command, refresh schedule, or native provisioning hook is enabled here.
Existing provisioning still uses `init-aks-cloud.sh`.
These are internal capabilities only. Existing provisioning still uses
`init-aks-cloud.sh`.

### Azure cloud certificate retrieval capabilities

WireServer helpers retrieve legacy certificate packages and RCV1P root/intermediate
operation groups. They preserve resource-name encoding, bounded requests/retries,
flat download caching, and per-certificate download skipping.

Retrieval does not install certificates or make opt-in decisions. Refresh
orchestration, the standalone refresh command, scheduling, and native provisioning
activation are supplied by later layers; they are not enabled here.

### Validating and normalizing AKSNodeConfig

Expand Down
183 changes: 183 additions & 0 deletions aks-node-controller/cloud_certificate_transport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package main

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"maps"
"net/http"
"net/url"
"path/filepath"
"strings"
"time"

"github.com/Azure/agentbaker/aks-node-controller/common"
)

const cloudRequestAttempts = 10

func (c *cloudInitializer) requestCertificates(ctx context.Context, path string) ([]byte, error) {
attempt := 0
delay, nextDelay := 3*time.Second, time.Duration(0)
return common.RetryFetch(ctx, cloudRequestAttempts, nil, func(ctx context.Context) ([]byte, error) {
if attempt > 0 {
if err := c.wait(ctx, nextDelay); err != nil {
return nil, err
}
}
attempt++
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.wireEndpoint+path, nil)
if err != nil {
return nil, err
}
nextDelay = delay
resp, err := c.wireClient.Do(req)
if err != nil {
return nil, err
}
body, err := readCloudResponse(resp)
if err != nil {
return nil, err
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return body, nil
}
if resp.StatusCode == http.StatusForbidden && bytes.Contains(body, []byte("RequestRateLimitExceeded")) {
delay *= 2
}
return nil, fmt.Errorf("WireServer certificate request %s returned HTTP %d", path, resp.StatusCode)
})
}

func certificatePackagePath(kind, extension string) string {
return "/machine?" + url.Values{
"comp": {"acmspackage"},
"type": {kind},
"ext": {extension},
}.Encode()
}

func validCloudCertificateName(name string) bool {
return name != "" && name != "." && name != ".." && filepath.Base(name) == name &&
!strings.ContainsRune(name, 0)
}

func (c *cloudInitializer) legacyCertificates(ctx context.Context) (map[string][]byte, error) {
body, err := c.requestCertificates(ctx, certificatePackagePath("cacertificates", "json"))
if err != nil {
return nil, fmt.Errorf("retrieve legacy Azure cloud certificates: %w", err)
}
var response struct {
Certificates []struct {
Name json.RawMessage
Body json.RawMessage `json:"CertBody"`
}
}
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("decode legacy Azure cloud certificates: %w", err)
}
if response.Certificates == nil {
return nil, errors.New("legacy Azure cloud response is missing a Certificates array")
}
certificates := make(map[string][]byte)
for _, cert := range response.Certificates {
var name string
if err := json.Unmarshal(cert.Name, &name); err != nil || !validCloudCertificateName(name) {
slog.Warn("rejecting invalid Azure cloud certificate filename")
continue
}
var certBody *string
if err := json.Unmarshal(cert.Body, &certBody); err != nil {
return nil, fmt.Errorf("decode legacy Azure cloud certificate body %s: %w", name, err)
}
if certBody == nil {
return nil, fmt.Errorf("legacy Azure cloud certificate %s is missing CertBody", name)
}
// Shell command substitution trims trailing LFs before CRLF normalization.
data := strings.ReplaceAll(strings.TrimRight(*certBody, "\n"), "\r\n", "\n") + "\n"
name = strings.ReplaceAll(name, ".cer", ".crt")
if err := c.writeFile("/root/AzureCACertificates/"+name, []byte(data)); err != nil {
return nil, fmt.Errorf("cache legacy Azure cloud certificate %s: %w", name, err)
}
certificates[name] = []byte(data)
}
return certificates, nil
}

func (c *cloudInitializer) operationCertificates(ctx context.Context) (map[string][]byte, error) {
certificates := make(map[string][]byte)
for _, kind := range []string{"operationrequestsroot", "operationrequestsintermediate"} {
group, err := c.operationCertificateGroup(ctx, kind)
if err != nil {
return nil, err
}
maps.Copy(certificates, group)
}
return certificates, nil
}

func (c *cloudInitializer) operationCertificateGroup(ctx context.Context, kind string) (map[string][]byte, error) {
body, err := c.requestCertificates(ctx, certificatePackagePath(kind, "json"))
if err != nil {
return nil, fmt.Errorf("retrieve Azure cloud %s: %w", kind, err)
}
var response struct {
Operations []struct {
// ResouceFileName is the spelling used by the WireServer protocol.
Name json.RawMessage `json:"ResouceFileName"`
} `json:"OperationsInfo"`
}
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("decode Azure cloud %s: %w", kind, err)
}
certificates := make(map[string][]byte)
found := false
for _, operation := range response.Operations {
var name string
if err := json.Unmarshal(operation.Name, &name); err != nil || name == "" {
slog.Warn("ignoring missing or non-string Azure cloud certificate filename")
continue
}
found = true
if !validCloudCertificateName(name) {
slog.Warn("rejecting invalid Azure cloud certificate filename")
continue
}
data, err := c.operationCertificate(ctx, name)
if err != nil {
if ctx.Err() != nil {
return nil, ctx.Err()
}
slog.Warn("could not retrieve Azure cloud certificate, continuing with remaining certificates", "name", name, "error", err)
continue
}
if err := c.writeFile("/root/AzureCACertificates/"+name, data); err != nil {
return nil, fmt.Errorf("cache Azure cloud certificate %s: %w", name, err)
}
certificates[name] = data
}
if !found {
return nil, fmt.Errorf("cloud %s contains no certificate filenames", kind)
}
return certificates, nil
}

func (c *cloudInitializer) operationCertificate(ctx context.Context, filename string) ([]byte, error) {
name, extension := filename, filename
if suffix := filepath.Ext(filename); suffix != "" {
name = strings.TrimSuffix(filename, suffix)
extension = strings.TrimPrefix(suffix, ".")
}
body, err := c.requestCertificates(ctx, certificatePackagePath(name, extension))
if err != nil {
return nil, fmt.Errorf("retrieve Azure cloud certificate %s: %w", filename, err)
}
body = bytes.TrimRight(body, "\n")
if len(body) == 0 {
return nil, fmt.Errorf("cloud certificate %s response is empty", filename)
}
return append(body, '\n'), nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package main

import (
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"testing"

"github.com/stretchr/testify/require"
)

func TestCloudLegacyCertificateTransport(t *testing.T) {
c := newTestCloudInitializer(t, cloudTestUbuntu)
c.run = func(*exec.Cmd) error {
t.Fatal("certificate retrieval must not invoke trust-store tools")
return nil
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
if r.URL.Path != "/machine" || query.Get("comp") != "acmspackage" ||
query.Get("type") != "cacertificates" || query.Get("ext") != "json" {
t.Errorf("unexpected legacy certificate request: %s", r.URL)
w.WriteHeader(http.StatusBadRequest)
return
}
_, _ = io.WriteString(w, `{"Certificates":[{"Name":"root.cer","CertBody":"first\r\nsecond\r\n"},{"Name":"../escape.cer","CertBody":"ignored"}]}`)
}))
defer server.Close()
c.wireEndpoint = server.URL
certificates, err := c.legacyCertificates(t.Context())
require.NoError(t, err)
payload := []byte("first\nsecond\r\n")
require.Equal(t, map[string][]byte{"root.crt": payload}, certificates)
cached, err := os.ReadFile(c.path("/root/AzureCACertificates/root.crt"))
require.NoError(t, err)
require.Equal(t, payload, cached)
_, err = os.Stat(c.path("/root/escape.crt"))
require.ErrorIs(t, err, os.ErrNotExist)
}

func TestCloudOperationCertificateTransport(t *testing.T) {
c := newTestCloudInitializer(t, "ID=azurelinux\n")
c.run = func(*exec.Cmd) error {
t.Fatal("certificate retrieval must not invoke trust-store tools")
return nil
}
serveCloudOperationTestResponses(t, c,
[]string{"missing.crt", "empty.crt", "root+ca.crt", "unfiltered.cer"},
[]string{"intermediate.crt"})
certificates, err := c.operationCertificates(t.Context())
require.NoError(t, err)
payload := []byte("certificate payload\n")
require.Equal(t, map[string][]byte{
"root+ca.crt": payload,
"unfiltered.cer": payload,
"intermediate.crt": payload,
}, certificates)
for name, want := range certificates {
cached, readErr := os.ReadFile(c.path("/root/AzureCACertificates/" + name))
require.NoError(t, readErr)
require.Equal(t, want, cached)
}
_, err = os.Stat(c.path("/etc/pki/ca-trust/source/anchors"))
require.ErrorIs(t, err, os.ErrNotExist)
}

func TestCloudCertificateTransportEncodesResourceNames(t *testing.T) {
c := newTestCloudInitializer(t, cloudTestUbuntu)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
if query.Get("type") != "root+ca" || query.Get("ext") != "crt" {
t.Errorf("incorrectly encoded certificate resource: %s", r.URL)
w.WriteHeader(http.StatusBadRequest)
return
}
_, _ = io.WriteString(w, "raw certificate payload\n\n")
}))
defer server.Close()
c.wireEndpoint = server.URL
payload, err := c.operationCertificate(t.Context(), "root+ca.crt")
require.NoError(t, err)
require.Equal(t, "raw certificate payload\n", string(payload))
}
Loading
Loading