Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
11 changes: 6 additions & 5 deletions server/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,11 @@ func main() {
var otlpExporter api.OTLPExporter
var otlpMetrics *events.OTLPMetrics
if config.OTLPEndpoint != "" {
headers := map[string]string{}
if config.InstanceJWT != "" {
headers["Authorization"] = "Bearer " + config.InstanceJWT
}
// The relay authenticates the VM by its instance JWT, sent as a bearer
// token. The credential is resolved per request: on a forked VM the boot
// env JWT is stale, so it is re-read from the applied fork-identity
// payload (see instanceJWTProvider).
jwtProvider := newInstanceJWTProvider(config.InstanceJWT)
slogger.Info("OTLP export available", "endpoint", config.OTLPEndpoint, "path", config.OTLPPath)
// The OTel log SDK reports batch-queue drops through its global logger at
// logr V(1), which slog renders just below Info, so a default Info handler
Expand All @@ -156,7 +157,7 @@ func main() {
Endpoint: config.OTLPEndpoint,
URLPath: config.OTLPPath,
Insecure: config.OTLPInsecure,
Headers: headers,
AuthTokenFunc: jwtProvider.Token,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auth header mismatches relay contract

High Severity

The OTLP exporter sends an Authorization: Bearer token, but the relay expects an x-api-key header with the instance name. This authentication mismatch causes OTLP exports to fail, preventing session telemetry from landing.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a6d280d. Configure here.

Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
ServiceName: config.OTLPServiceName,
InstanceName: config.InstanceName,
Metro: config.MetroName,
Expand Down
70 changes: 70 additions & 0 deletions server/cmd/api/otlp_credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"sync"
"time"

"github.com/kernel/kernel-images/server/lib/forkidentity"
)

// instanceJWTCacheTTL bounds how often the provider re-reads the applied
// fork-identity payload from disk. Export requests are frequent; the JWT changes
// at most once per fork, so a few seconds of staleness is harmless and avoids a
// filesystem read on every request.
const instanceJWTCacheTTL = 3 * time.Second

// instanceJWTProvider resolves the OTLP relay bearer credential per request.
//
// In fork-identity-wait mode the platform delivers a fresh KERNEL_INSTANCE_JWT
// in the applied identity payload after kernel-images-api has already started,
// and the process is not restarted, so the boot-time env JWT is stale (empty on
// an unbound fork). This provider reads the JWT from the applied payload instead,
// falling back to the boot env otherwise. Outside fork mode it returns the boot
// JWT unchanged.
type instanceJWTProvider struct {
bootJWT string
forkEnabled bool

mu sync.Mutex
cached string
cachedAt time.Time
}

func newInstanceJWTProvider(bootJWT string) *instanceJWTProvider {
enabled, _ := forkidentity.WaitEnabled()
return &instanceJWTProvider{bootJWT: bootJWT, forkEnabled: enabled}
}
Comment thread
cursor[bot] marked this conversation as resolved.

// Token returns the current bearer credential, or "" when none is available yet
// (the caller then sends no Authorization header rather than an empty bearer).
func (p *instanceJWTProvider) Token() string {
if !p.forkEnabled {
return p.bootJWT
}
p.mu.Lock()
defer p.mu.Unlock()
if !p.cachedAt.IsZero() && time.Since(p.cachedAt) < instanceJWTCacheTTL {
return p.cached
}
p.cached = p.resolve()
p.cachedAt = time.Now()
return p.cached
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
}

// resolve reads the applied fork-identity payload for the JWT. It only reads once
// the applied marker is present, to avoid a torn read while the wrapper is
// mid-apply, and falls back to the boot JWT on any miss.
func (p *instanceJWTProvider) resolve() string {
applied, err := forkidentity.ReadAppliedMarker()
if err != nil || applied == "" {
return p.bootJWT
}
payload, err := forkidentity.ReadPayload()
if err != nil {
return p.bootJWT
}
if jwt := payload.Get("kernel_instance_jwt"); jwt != "" {
return jwt
}
return p.bootJWT
}
72 changes: 72 additions & 0 deletions server/cmd/api/otlp_credential_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package main

import (
"path/filepath"
"testing"

"github.com/kernel/kernel-images/server/lib/forkidentity"
)

// withForkIdentityFiles points the forkidentity package vars at temp paths for
// the duration of a test and restores them after.
func withForkIdentityFiles(t *testing.T) {
t.Helper()
dir := t.TempDir()
payload, applied := forkidentity.PayloadFile, forkidentity.AppliedFile
forkidentity.PayloadFile = filepath.Join(dir, "fork-identity.json")
forkidentity.AppliedFile = filepath.Join(dir, "fork-identity-applied")
t.Cleanup(func() {
forkidentity.PayloadFile = payload
forkidentity.AppliedFile = applied
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate fork-identity test helper

Low Severity

withForkIdentityFiles duplicates useTempForkIdentityFiles in the same main package. Both retarget forkidentity.PayloadFile / AppliedFile to a temp dir and restore them on cleanup, so future path-handling fixes can easily drift between the two helpers.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3c9a8e5. Configure here.


func writeAppliedPayload(t *testing.T, jwt string) {
t.Helper()
p := forkidentity.Payload{
"instance_name": "browser-fork-1",
"session_intel_url": "https://intel.example",
"kernel_instance_jwt": jwt,
}
if err := forkidentity.WritePayload(p); err != nil {
t.Fatalf("write payload: %v", err)
}
if err := forkidentity.WriteAppliedMarker(p.InstanceName()); err != nil {
t.Fatalf("write applied marker: %v", err)
}
}

func TestInstanceJWTProvider(t *testing.T) {
t.Run("non-fork returns boot jwt", func(t *testing.T) {
withForkIdentityFiles(t)
t.Setenv(forkidentity.WaitEnv, "")
// Even with an applied payload present, non-fork mode ignores it.
writeAppliedPayload(t, "payload-jwt")
p := newInstanceJWTProvider("boot-jwt")
if got := p.Token(); got != "boot-jwt" {
t.Errorf("Token() = %q, want boot-jwt", got)
}
})

t.Run("fork before apply falls back to boot jwt", func(t *testing.T) {
withForkIdentityFiles(t)
t.Setenv(forkidentity.WaitEnv, "true")
// No payload/applied marker written yet: mid-apply, must not read a torn value.
p := newInstanceJWTProvider("boot-jwt")
if got := p.Token(); got != "boot-jwt" {
t.Errorf("Token() = %q, want boot-jwt", got)
}
})

t.Run("fork after apply returns fresh payload jwt", func(t *testing.T) {
withForkIdentityFiles(t)
t.Setenv(forkidentity.WaitEnv, "true")
writeAppliedPayload(t, "fresh-jwt")
// Boot jwt is empty, mirroring an unbound fork; the fresh credential must
// come from the applied payload.
p := newInstanceJWTProvider("")
if got := p.Token(); got != "fresh-jwt" {
t.Errorf("Token() = %q, want fresh-jwt", got)
}
})
}
Loading
Loading