feat: serve the deployment CA from IMDS instead of baking it into images (#784)

Image import shelled out to virt-customize to install the deployment CA into
an imported image's trust store. The step was best-effort and skipped whenever
virt-customize was absent, which is the default: nothing in provisioning
installs guestfs-tools on a node. When it did run it mutated the image after
its checksum had been verified, it could not handle Windows guests, and it tied
CA rotation to re-importing every affected AMI.

Nothing the stack launches depended on it. ECS container instances, EKS k3s
servers and BootDirect microVMs all receive the gateway CA at launch via
user-data or fw_cfg, and boot itself needs no CA because IMDS is plain HTTP.
The only uncovered case is a customer instance whose own workload calls a
Spinifex HTTPS endpoint, which needs a pull mechanism rather than an image
mutation.

The IMDS responder now serves the CA at /spinifex/ca.pem, outside the /latest
tree so the AWS-compatible surface stays byte-identical to EC2's. The route is
token-free because a CA certificate is public material and the console already
serves it unauthenticated; the X-Forwarded-For SSRF guard still applies. The
PEM is cached and re-read on modtime change, so rotating ca.pem on a node
reaches guests without restarting vpcd.
This commit is contained in:
Josh Siviour
2026-08-13 10:55:31 +10:00
committed by GitHub
parent 65d2501e6b
commit 58e0f2879f
10 changed files with 252 additions and 134 deletions
-65
View File
@@ -690,13 +690,6 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) {
defer nc.Close()
node := appConfig.Nodes[appConfig.Node]
// Bake the deployment CA into the image trust store so a stock cloud image
// trusts the gateway from first boot. Best-effort: never fails the import.
// The CA path derives from the --spinifex-dir data root (its config/ symlink
// holds ca.pem), not NodeBaseDir(): node.BaseDir is unset in the import
// command, which would collapse the path to a relative config/ca.pem.
bakeCACertIntoImage(extractedImagePath, filepath.Join(baseDir, "config", "ca.pem"))
fmt.Println("Writing image to storage ...")
provider := ebsprovider.NewNATSProvider(nc, imageImportTimeout)
err = admin.ImportImage(context.Background(), provider, admin.ImportOpts{
@@ -758,64 +751,6 @@ func registerImportedAMISnapshot(store objectstore.ObjectStore, bucket string, a
return nil
}
// caBakeRunCommand installs the uploaded CA into the guest trust store across
// the distro families, then removes the staged copy. Each branch is gated on its
// updater existing so only the matching distro's branch writes anything (install
// -D creates the anchor dir on minimal images). Debian/Ubuntu use
// update-ca-certificates, RHEL/Rocky use update-ca-trust, and stock Alpine ships
// only ca-certificates-bundle (no updater, no anchor dir) so it falls back to
// appending the PEM to the static bundle. The install chain's exit code is
// preserved past the cleanup rm so a failure surfaces instead of a false success.
// The RHEL branch restorecon's only the trust-store paths it touched so the cert
// is correctly labelled without the image-wide --no-selinux-relabel (below)
// leaving an unlabelled bundle; restorecon is best-effort and never fails the bake.
const caBakeRunCommand = `( { command -v update-ca-certificates >/dev/null && install -D -m644 /tmp/spinifex-ca.pem /usr/local/share/ca-certificates/spinifex.crt && update-ca-certificates; } || ` +
`{ command -v update-ca-trust >/dev/null && install -D -m644 /tmp/spinifex-ca.pem /etc/pki/ca-trust/source/anchors/spinifex.crt && update-ca-trust && ` +
`{ command -v restorecon >/dev/null 2>&1 && restorecon -RF /etc/pki/ca-trust/source/anchors/spinifex.crt /etc/pki/ca-trust/extracted >/dev/null 2>&1; true; }; } || ` +
`{ cat /tmp/spinifex-ca.pem >> /etc/ssl/certs/ca-certificates.crt; } ); ` +
`rc=$?; rm -f /tmp/spinifex-ca.pem; exit $rc`
// caBakeTimeout bounds the virt-customize run so a stalled libguestfs appliance
// cannot hang the import indefinitely; on timeout the bake degrades to a skip.
const caBakeTimeout = 5 * time.Minute
// caBakeCmd builds the virt-customize invocation that uploads the deployment CA
// into the disk image at imagePath and installs it into the guest trust store.
// --no-selinux-relabel stops virt-customize flagging the image for a first-boot
// SELinux autorelabel: that relabel+reboot corrupts XFS roots (RHEL/Rocky) on the
// reboot, so the run-command relabels only the touched trust paths instead.
func caBakeCmd(ctx context.Context, imagePath, caCertPath string) *exec.Cmd {
return exec.CommandContext(ctx, "virt-customize", "-a", imagePath,
"--no-selinux-relabel",
"--upload", caCertPath+":/tmp/spinifex-ca.pem",
"--run-command", caBakeRunCommand)
}
// caBakeRunner resolves virt-customize and runs the CA bake; overridable in tests.
var caBakeRunner = func(imagePath, caCertPath string) ([]byte, error) {
if _, err := exec.LookPath("virt-customize"); err != nil {
return nil, fmt.Errorf("virt-customize not found: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), caBakeTimeout)
defer cancel()
return caBakeCmd(ctx, imagePath, caCertPath).CombinedOutput()
}
// bakeCACertIntoImage uploads the deployment CA into the image's trust store via
// virt-customize so an imported stock image trusts the gateway from first boot.
// Best-effort: a missing CA, an absent virt-customize, or an image libguestfs
// cannot inspect logs and continues — image import never fails on the CA.
func bakeCACertIntoImage(imagePath, caCertPath string) {
if _, err := os.Stat(caCertPath); err != nil {
slog.Warn("CA bake skipped: deployment CA not found; imported image will not auto-trust the gateway", "ca", caCertPath, "err", err)
return
}
if out, err := caBakeRunner(imagePath, caCertPath); err != nil {
slog.Warn("CA bake skipped: virt-customize could not customize image; imported image will not auto-trust the gateway", "err", err, "output", string(out))
return
}
}
func runVolumesOrphansCmd(_ *cobra.Command, _ []string) {
appConfig, nc, err := loadConfigAndConnect()
if err != nil {
-67
View File
@@ -1,67 +0,0 @@
package cmd
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCABakeCmd_Args(t *testing.T) {
cmd := caBakeCmd(context.Background(), "/img/disk.raw", "/data/config/ca.pem")
args := cmd.Args
assert.Equal(t, "virt-customize", filepath.Base(args[0]))
assert.Contains(t, args, "-a")
assert.Contains(t, args, "/img/disk.raw")
assert.Contains(t, args, "--upload")
assert.Contains(t, args, "/data/config/ca.pem:/tmp/spinifex-ca.pem")
assert.Contains(t, args, "--run-command")
// --no-selinux-relabel stops virt-customize forcing a first-boot autorelabel,
// whose relabel+reboot corrupts XFS roots on RHEL/Rocky.
assert.Contains(t, args, "--no-selinux-relabel")
// The run-command covers Debian/Ubuntu (update-ca-certificates), RHEL/Rocky
// (update-ca-trust), and the stock-Alpine fallback that appends to the static
// ca-certificates bundle when no updater is present. The RHEL branch relabels
// only the touched trust paths so the cert is labelled without a full relabel.
joined := strings.Join(args, " ")
assert.Contains(t, joined, "update-ca-certificates")
assert.Contains(t, joined, "update-ca-trust")
assert.Contains(t, joined, "restorecon")
assert.Contains(t, joined, "/etc/ssl/certs/ca-certificates.crt")
}
func TestBakeCACertIntoImage_SkipsWhenCAMissing(t *testing.T) {
called := false
orig := caBakeRunner
caBakeRunner = func(string, string) ([]byte, error) { called = true; return nil, nil }
defer func() { caBakeRunner = orig }()
// A missing CA must not invoke virt-customize and must not panic.
bakeCACertIntoImage("/img/disk.raw", filepath.Join(t.TempDir(), "absent-ca.pem"))
assert.False(t, called, "runner must not be called when the CA is absent")
}
func TestBakeCACertIntoImage_ContinuesOnRunnerError(t *testing.T) {
caPath := filepath.Join(t.TempDir(), "ca.pem")
require.NoError(t, os.WriteFile(caPath, []byte("ca"), 0600))
called := false
orig := caBakeRunner
caBakeRunner = func(string, string) ([]byte, error) {
called = true
return []byte("libguestfs: cannot inspect image"), errors.New("exit status 1")
}
defer func() { caBakeRunner = orig }()
// An image libguestfs cannot inspect must skip-and-continue, never panic/fail.
bakeCACertIntoImage("/img/disk.raw", caPath)
assert.True(t, called, "runner must be invoked when the CA is present")
}
+1
View File
@@ -981,6 +981,7 @@ var vpcdStartCmd = &cobra.Command{
NatsHost: nodeConfig.NATS.Host,
NatsToken: nodeConfig.NATS.ACL.Token,
NatsCACert: nodeConfig.NATS.CACert,
CACert: nodeConfig.NATS.CACert,
OVNNBAddr: nodeConfig.VPCD.OVNNBAddr,
OVNSBAddr: nodeConfig.VPCD.OVNSBAddr,
BaseDir: nodeConfig.BaseDir,
+25 -1
View File
@@ -98,7 +98,7 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
| Command | Flags | Description |
|---------|-------|-------------|
| `spx admin images import` | `--name`, `--file`, `--distro`, `--version`, `--arch`, `--platform`, `--boot-mode` (bios/uefi/uefi-preferred), `--tag`, `--force`, `--skip-verify` | Catalog imports (`--name`) download the image, fetch the catalog `Checksum` URL, verify the SHA-256/SHA-512 digest, and inherit `BootMode` from the catalog entry. `--boot-mode` overrides the catalog value when set. Mismatch fails closed; the cached file is left on disk and `--force` re-downloads. `--file` imports skip checksum verification (operator-supplied media is outside Spinifex's trust boundary, the skip is logged at INFO for audit) and require an explicit `--boot-mode` because there is no catalog metadata to inherit from. `--skip-verify` bypasses verification for catalog imports and emits a WARN slog + stderr notice; use only for debugging or when upstream mirrors are confirmed-broken. On every import a best-effort `virt-customize` step bakes the deployment CA (`<data-root>/config/ca.pem`) into the image's trust store before the block copy, so in-guest SDK-over-TLS calls to Spinifex endpoints trust the gateway from first boot; an image libguestfs cannot customize is imported as-is (CA-free) and logs a skip. Because the CA is fixed into the image, rotating the cluster CA requires re-importing affected images. |
| `spx admin images import` | `--name`, `--file`, `--distro`, `--version`, `--arch`, `--platform`, `--boot-mode` (bios/uefi/uefi-preferred), `--tag`, `--force`, `--skip-verify` | Catalog imports (`--name`) download the image, fetch the catalog `Checksum` URL, verify the SHA-256/SHA-512 digest, and inherit `BootMode` from the catalog entry. `--boot-mode` overrides the catalog value when set. Mismatch fails closed; the cached file is left on disk and `--force` re-downloads. `--file` imports skip checksum verification (operator-supplied media is outside Spinifex's trust boundary, the skip is logged at INFO for audit) and require an explicit `--boot-mode` because there is no catalog metadata to inherit from. `--skip-verify` bypasses verification for catalog imports and emits a WARN slog + stderr notice; use only for debugging or when upstream mirrors are confirmed-broken. Import never modifies the image: the bytes written to storage are the bytes that were verified. A guest that needs to trust the deployment CA for in-guest SDK-over-TLS calls fetches it at runtime from `http://169.254.169.254/spinifex/ca.pem` (see the IMDS section), so a CA rotation reaches guests without re-importing anything. |
| `spx admin images list` | — | Lists available OS images that can be imported or downloaded |
| `spx admin images promote` | `--image-id` (required), `--yes` | Reads `ami-<id>/config.json`, validates the AMI is account-owned, then rewrites `ImageOwnerAlias` to `"system"` in-place. No block data is copied. The change takes effect immediately — the AMI becomes visible to all accounts via `DescribeImages`. Prompts for confirmation (skipped with `--yes`). Already-system AMIs are refused. |
| `spx admin images remove` | `--image-id` (required), `--force`, `--yes` | Loads `ami-<id>/config.json`, walks transitive dependents — copied snapshots whose `VolumeID == imageID`, volumes whose `SnapshotID` references the internal `snap-ami-<id>` or any derived snap, and account AMIs created via `CopyImage` whose `SnapshotID` is a derived snap — then prompts (skipped with `--yes`) before deleting `ami-<id>/config.json` (the DescribeImages barrier) followed by the rest of `ami-<id>/` and `snap-ami-<id>/`. Account-owned AMIs are refused with a hint pointing at `aws ec2 deregister-image` + `aws ec2 delete-snapshot`. `--force` bypasses the dependency, ownership and config-corrupt checks for salvage of orphaned blocks. |
@@ -671,6 +671,29 @@ curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
curl -i http://169.254.169.254/latest/meta-data/instance-id
```
### IMDS — Deployment CA
A guest that calls a Spinifex HTTPS endpoint with an AWS SDK needs the deployment CA in its trust
store. The CA is served from the IMDS responder at `/spinifex/ca.pem` — deliberately outside the
`/latest` tree, because the AWS-compatible surface stays byte-identical to EC2's and cloud-init must
never descend into a Spinifex-only path.
The route is **token-free**: a CA certificate is public material (the console already serves it
unauthenticated at `/api/ca.pem`), and requiring the IMDSv2 handshake would push a token dance into
every bootstrap snippet. The `X-Forwarded-For` SSRF guard still applies, as on every other path.
```yaml
#cloud-config
runcmd:
# Debian/Ubuntu
- curl -fsS http://169.254.169.254/spinifex/ca.pem -o /usr/local/share/ca-certificates/spinifex.crt
- update-ca-certificates
```
RHEL/Rocky write to `/etc/pki/ca-trust/source/anchors/spinifex.crt` and run `update-ca-trust`; stock
Alpine has no updater or anchor directory, so append the PEM to `/etc/ssl/certs/ca-certificates.crt`.
Rotating `ca.pem` on a node is served immediately — vpcd re-reads the file when it changes.
### IMDS — Supported Paths
| Path | Method | Source | Status |
@@ -702,6 +725,7 @@ curl -i http://169.254.169.254/latest/meta-data/instance-id
| `/latest/meta-data/public-keys/0/` | GET | `openssh-key` (format list for index 0) | **DONE** |
| `/latest/meta-data/public-keys/0/openssh-key` | GET | Launch SSH public key, live-fetched from the key store; 404 if the key was deleted, 500 on backend fault | **DONE** |
| `/latest/user-data` | GET | `vm.UserData`; 404 if none | **DONE** |
| `/spinifex/ca.pem` | GET | Deployment CA PEM (`application/x-pem-file`); token-free, outside the AWS tree; 404 when no CA is configured | **DONE** |
| `/latest/dynamic` | GET | Lists `instance-identity/` | **DONE** |
| `/latest/dynamic/instance-identity` | GET | Lists `document` (signed forms listed when the signing key lands) | **DONE** |
| `/latest/dynamic/instance-identity/document` | GET | Unsigned identity document from resolved ENI + instance facts | **DONE** |
+10
View File
@@ -312,11 +312,21 @@ sudo ss -tlnp | grep 6642
### CA Certificate Not Trusted
On a node or any host running `spx`/`aws` against the cluster:
```bash
sudo cp /etc/spinifex/ca.pem /usr/local/share/ca-certificates/spinifex-ca.crt
sudo update-ca-certificates
```
Inside a guest VM there is no `/etc/spinifex`; fetch the CA from IMDS instead:
```bash
sudo curl -fsS http://169.254.169.254/spinifex/ca.pem \
-o /usr/local/share/ca-certificates/spinifex-ca.crt
sudo update-ca-certificates
```
### Cross-Host VMs Cannot Communicate
```bash
+10
View File
@@ -85,7 +85,17 @@ export PATH=$PATH:/usr/local/go/bin
### CA Certificate Not Trusted
On a node or any host running `spx`/`aws` against the cluster:
```bash
sudo cp /etc/spinifex/ca.pem /usr/local/share/ca-certificates/spinifex-ca.crt
sudo update-ca-certificates
```
Inside a guest VM there is no `/etc/spinifex`; fetch the CA from IMDS instead:
```bash
sudo curl -fsS http://169.254.169.254/spinifex/ca.pem \
-o /usr/local/share/ca-certificates/spinifex-ca.crt
sudo update-ca-certificates
```
+87
View File
@@ -0,0 +1,87 @@
package handlers_imds
import (
"errors"
"log/slog"
"net/http"
"os"
"sync"
"time"
)
// pathSpinifexCACert serves the deployment CA. It sits outside the /latest tree
// because it is a Spinifex extension: the AWS-compatible metadata surface must
// stay byte-identical to EC2's, so no new key is added under /latest/meta-data.
const pathSpinifexCACert = "/spinifex/ca.pem"
// caCertContentType matches the console's /api/ca.pem endpoint.
const caCertContentType = "application/x-pem-file"
// caCertCache serves the deployment CA from disk, re-reading only when the file
// changes so a CA rotation is picked up without restarting vpcd.
type caCertCache struct {
path string
mu sync.Mutex
pem []byte
modTime time.Time
size int64
}
func newCACertCache(path string) *caCertCache {
return &caCertCache{path: path}
}
// load returns the current CA PEM, re-reading when modtime or size moved.
func (c *caCertCache) load() ([]byte, error) {
if c.path == "" {
return nil, errors.New("no CA certificate path configured")
}
fi, err := os.Stat(c.path)
if err != nil {
return nil, err
}
c.mu.Lock()
defer c.mu.Unlock()
if c.pem != nil && fi.ModTime().Equal(c.modTime) && fi.Size() == c.size {
return c.pem, nil
}
pem, err := os.ReadFile(c.path)
if err != nil {
return nil, err
}
c.pem, c.modTime, c.size = pem, fi.ModTime(), fi.Size()
return c.pem, nil
}
// handleCACert serves the deployment CA so a guest can install it into its own
// trust store. Deliberately token-free: a CA certificate is public material,
// already downloadable unauthenticated from the console, and requiring an
// IMDSv2 token would push the PUT handshake into every bootstrap snippet.
// rejectForwarded still wraps the mux, so the SSRF guard is unchanged, and a
// relayed request would yield a public certificate and no credential.
func (s *IMDSServiceImpl) handleCACert(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", http.MethodGet)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
eni := s.resolveCaller(r)
if eni == nil {
w.WriteHeader(http.StatusNotFound)
return
}
pem, err := s.caCert.load()
if err != nil {
slog.WarnContext(r.Context(), "IMDS: deployment CA unavailable", "instance_id", eni.instanceID, "err", err)
w.WriteHeader(http.StatusNotFound)
return
}
slog.InfoContext(r.Context(), "IMDS: serving deployment CA", "instance_id", eni.instanceID, "private_ip", eni.privateIP)
w.Header().Set("Content-Type", caCertContentType)
_, _ = w.Write(pem)
}
+110
View File
@@ -0,0 +1,110 @@
package handlers_imds
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const testCAPEM = "-----BEGIN CERTIFICATE-----\ntestca\n-----END CERTIFICATE-----\n"
// newCATestService builds a service whose CA cache points at a temp file holding
// pem, returning the handler and the file path so a test can rotate it.
func newCATestService(t *testing.T, pem string) (http.Handler, string) {
t.Helper()
path := filepath.Join(t.TempDir(), "ca.pem")
if pem != "" {
require.NoError(t, os.WriteFile(path, []byte(pem), 0o600))
}
svc, _ := newTestService(&fakeResolver{eni: testENI()}, &fakeIAM{}, &fakeAssumer{})
svc.caCert = newCACertCache(path)
return withTapENI(svc.httpHandler(), testENI()), path
}
// TestCACert_ServedWithoutToken is the load-bearing assertion for the route:
// the CA is public material, so a guest fetches it with a plain GET and no
// IMDSv2 token handshake. Every other metadata path 401s under those conditions.
func TestCACert_ServedWithoutToken(t *testing.T) {
h, _ := newCATestService(t, testCAPEM)
rec := get(t, h, pathSpinifexCACert, "")
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, testCAPEM, rec.Body.String())
assert.Equal(t, caCertContentType, rec.Header().Get("Content-Type"))
}
// TestCACert_MissingFileIs404 pins the fail-soft behaviour: a deployment with no
// CA on disk serves a 404, never a 500, and vpcd still starts.
func TestCACert_MissingFileIs404(t *testing.T) {
h, _ := newCATestService(t, "")
rec := get(t, h, pathSpinifexCACert, "")
assert.Equal(t, http.StatusNotFound, rec.Code)
}
// TestCACert_RotationServedWithoutRestart covers the reason the cache keys on
// modtime+size: rotating the CA on disk must reach guests without restarting vpcd.
func TestCACert_RotationServedWithoutRestart(t *testing.T) {
h, path := newCATestService(t, testCAPEM)
require.Equal(t, http.StatusOK, get(t, h, pathSpinifexCACert, "").Code)
rotated := "-----BEGIN CERTIFICATE-----\nrotated\n-----END CERTIFICATE-----\n"
require.NoError(t, os.WriteFile(path, []byte(rotated), 0o600))
require.NoError(t, os.Chtimes(path, time.Now().Add(time.Second), time.Now().Add(time.Second)))
rec := get(t, h, pathSpinifexCACert, "")
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, rotated, rec.Body.String())
}
// TestCACert_ForwardedRejected confirms the token-free route did not opt out of
// the SSRF guard: rejectForwarded wraps the whole mux, this path included.
func TestCACert_ForwardedRejected(t *testing.T) {
h, _ := newCATestService(t, testCAPEM)
req := httptest.NewRequest(http.MethodGet, "http://"+MetaDataServerIP+pathSpinifexCACert, nil)
req.RemoteAddr = testIP + ":50000"
req.Header.Set(hdrForwardedFor, "203.0.113.99")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
assert.Equal(t, http.StatusForbidden, rec.Code)
assert.Empty(t, rec.Body.String())
}
// TestCACert_RejectsNonGET keeps the route read-only.
func TestCACert_RejectsNonGET(t *testing.T) {
h, _ := newCATestService(t, testCAPEM)
req := httptest.NewRequest(http.MethodPost, "http://"+MetaDataServerIP+pathSpinifexCACert, nil)
req.RemoteAddr = testIP + ":50000"
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
assert.Equal(t, http.MethodGet, rec.Header().Get("Allow"))
}
// TestCACert_AbsentFromAWSMetadataListing pins the placement decision: the route
// is a Spinifex extension outside /latest, so the AWS-compatible surface a guest
// crawls is byte-identical to EC2's and cloud-init never descends into it.
func TestCACert_AbsentFromAWSMetadataListing(t *testing.T) {
h, _ := newCATestService(t, testCAPEM)
token := issueToken(t, h)
rec := get(t, h, pathMetaDataRoot, token)
require.Equal(t, http.StatusOK, rec.Code)
assert.NotContains(t, rec.Body.String(), "spinifex")
assert.NotContains(t, rec.Body.String(), "ca.pem")
}
+5 -1
View File
@@ -71,6 +71,7 @@ type IMDSServiceImpl struct {
now func() time.Time
baseDomain string
internalDomain string
caCert *caCertCache
}
// NewIMDSServiceImpl wires the IMDS service. listTaps is injected to avoid a
@@ -80,8 +81,9 @@ type IMDSServiceImpl struct {
// publishes. resolverIPs are the WAN IPs of nodes running northstar: when
// non-empty, each per-tap responder also serves the VPC DNS shim on
// 169.254.169.253:53, relaying to northstar's unprivileged wildcard listener.
// caCertPath is the deployment CA served at /spinifex/ca.pem; empty 404s it.
// ctx bounds the bucket opens only; each served request carries its own.
func NewIMDSServiceImpl(ctx context.Context, natsConn *nats.Conn, sts stsAssumer, iamSvc profileLookup, pubKeys publicKeyLookup, expectedNodes int, listTaps listTapsFunc, baseDomain, internalDomain string, resolverIPs []string) (*IMDSServiceImpl, error) {
func NewIMDSServiceImpl(ctx context.Context, natsConn *nats.Conn, sts stsAssumer, iamSvc profileLookup, pubKeys publicKeyLookup, expectedNodes int, listTaps listTapsFunc, baseDomain, internalDomain, caCertPath string, resolverIPs []string) (*IMDSServiceImpl, error) {
if natsConn == nil {
return nil, errors.New("nil NATS connection")
}
@@ -145,6 +147,7 @@ func NewIMDSServiceImpl(ctx context.Context, natsConn *nats.Conn, sts stsAssumer
now: time.Now,
baseDomain: baseDomain,
internalDomain: internalDomain,
caCert: newCACertCache(caCertPath),
}
// Each per-tap responder serves the shared mux, threading its tap's ENI
// identity into every request via BaseContext.
@@ -171,6 +174,7 @@ func NewIMDSServiceImpl(ctx context.Context, natsConn *nats.Conn, sts stsAssumer
func (s *IMDSServiceImpl) httpHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc(pathToken, s.handleToken)
mux.HandleFunc(pathSpinifexCACert, s.handleCACert)
mux.HandleFunc("/", s.handleMetadata)
return otelsetup.HTTPMiddleware("vpcd")(rejectForwarded(normalizeVersion(mux)))
}
+4
View File
@@ -97,6 +97,9 @@ type Config struct {
NatsToken string
// NatsCACert is the path to the CA certificate for NATS TLS.
NatsCACert string
// CACert is the path to the deployment CA. IMDS serves it at
// /spinifex/ca.pem so a guest can install it into its own trust store.
CACert string
// OVNNBAddr is the OVN Northbound DB address (e.g., "tcp:127.0.0.1:6641").
OVNNBAddr string
// OVNSBAddr is the OVN Southbound DB address (e.g., "tcp:127.0.0.1:6642"), used for monitoring.
@@ -522,6 +525,7 @@ func launchService(cfg *Config) error {
listTaps,
cfg.NorthstarBaseDomain,
cfg.NorthstarInternalDomain,
cfg.CACert,
cfg.ResolverNameservers,
)
if err != nil {