Bind service task HTTP input to authenticated caller before evaluation
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Failing after 1m1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 16:25:55 +02:00
parent 919ce25a37
commit 4f6946bbb3
8 changed files with 590 additions and 9 deletions

View file

@ -22,6 +22,7 @@ import (
"github.com/netkingdom/access-engine/internal/emission"
"github.com/netkingdom/access-engine/internal/policy"
"github.com/netkingdom/access-engine/internal/registry"
"github.com/netkingdom/access-engine/internal/taskexecution"
"github.com/netkingdom/access-engine/pkg/api"
)
@ -397,7 +398,11 @@ func runServe(args []string, stdout, stderr io.Writer) int {
return fail(stderr, err)
}
mux := newServeMuxWithCallerAuth(engine, authenticator)
serviceInputs, err := buildServicePilotInputs(engine, callerauth.Mode(*callerAuthMode), *callerAudience, *callerKubernetesURL, *callerReviewerTokenFile, *callerCAFile, callerBindings.StringMap())
if err != nil {
return fail(stderr, err)
}
mux := newServeMuxWithServicePilotInputs(engine, authenticator, serviceInputs)
if outbox != nil {
mountEmission(mux, outbox)
}
@ -448,6 +453,10 @@ func newServeMux(engine *decisioncore.Engine) *http.ServeMux {
}
func newServeMuxWithCallerAuth(engine *decisioncore.Engine, authenticator *callerauth.Authenticator) *http.ServeMux {
return newServeMuxWithServicePilotInputs(engine, authenticator, nil)
}
func newServeMuxWithServicePilotInputs(engine *decisioncore.Engine, authenticator *callerauth.Authenticator, serviceInputs *taskexecution.ServicePilotInputs) *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("content-type", "application/json")
@ -458,6 +467,25 @@ func newServeMuxWithCallerAuth(engine *decisioncore.Engine, authenticator *calle
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if engine.IsServicePilot() {
if serviceInputs == nil {
http.Error(w, "service input unavailable", http.StatusServiceUnavailable)
return
}
request, err := taskexecution.DecodeServiceRequest(r.Body)
if err != nil || request.Purpose != "" {
http.Error(w, "invalid service task request", http.StatusBadRequest)
return
}
input, err := serviceInputs.Prepare(r.Context(), r.Header.Get("Authorization"), request, time.Now().UTC())
if err != nil {
http.Error(w, "service task input refused", http.StatusForbidden)
return
}
decision, err := engine.CheckServicePilot(r.Context(), input)
writeHTTP(w, decision, err)
return
}
var request api.CheckRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@ -478,6 +506,10 @@ func newServeMuxWithCallerAuth(engine *decisioncore.Engine, authenticator *calle
writeHTTP(w, decision, err)
})
mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) {
if engine.IsServicePilot() {
http.Error(w, "service task batch unavailable", http.StatusForbidden)
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
@ -502,6 +534,22 @@ func newServeMuxWithCallerAuth(engine *decisioncore.Engine, authenticator *calle
return mux
}
// Only the existing exact service pilot package selects this source path.
// This is not an eligibility toggle; successful preparation still only DENYs.
func buildServicePilotInputs(engine *decisioncore.Engine, mode callerauth.Mode, audience, kubernetesURL, reviewerTokenFile, caFile string, bindings map[string]string) (*taskexecution.ServicePilotInputs, error) {
if !engine.IsServicePilot() {
return nil, nil
}
if mode != callerauth.ModeEnforce || audience != "flex-auth" || len(bindings) != 1 || bindings["secrets-engine"] != taskexecution.ServicePilotPrincipal {
return nil, fmt.Errorf("service pilot requires exact enforced retained caller mapping")
}
reviewer, err := callerauth.NewKubernetesTokenReviewer(kubernetesURL, audience, reviewerTokenFile, caFile)
if err != nil {
return nil, err
}
return taskexecution.NewServicePilotInputs(reviewer)
}
func buildCallerAuthenticator(mode callerauth.Mode, audience, kubernetesURL, reviewerTokenFile, caFile string, bindings map[string]string, stderr io.Writer) (*callerauth.Authenticator, error) {
if mode == callerauth.ModeDisabled {
return callerauth.New(mode, nil, "", nil, nil)
@ -615,7 +663,7 @@ func mountEmission(mux *http.ServeMux, outbox *emission.Outbox) {
func writeHTTP(w http.ResponseWriter, value any, err error) {
w.Header().Set("content-type", "application/json")
if errors.Is(err, decisioncore.ErrRecordNotCommitted) {
if errors.Is(err, decisioncore.ErrRecordNotCommitted) || errors.Is(err, decisioncore.ErrServicePilotUnavailable) {
// Unavailable evaluation is not an authorization verdict (GH-DEC-2026-024).
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return

View file

@ -0,0 +1,376 @@
package main
import (
"bytes"
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"encoding/pem"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/netkingdom/access-engine/internal/audit"
"github.com/netkingdom/access-engine/internal/callerauth"
"github.com/netkingdom/access-engine/internal/decision"
"github.com/netkingdom/access-engine/internal/policy"
"github.com/netkingdom/access-engine/internal/registry"
"github.com/netkingdom/access-engine/internal/sign"
"github.com/netkingdom/access-engine/internal/taskexecution"
"github.com/netkingdom/access-engine/pkg/api"
)
func pilotRequest(now time.Time) api.CheckRequest {
return api.CheckRequest{ID: "source-only-service-http", Tenant: "tenant:platform", Subject: api.SubjectRef{ID: "secrets-engine", Type: "service"}, Action: "execute-ccr", Resource: api.ResourceRef{ID: "catalog:synthetic", Type: "secret-catalog-lane", System: "secrets-engine", Attributes: map[string]any{"stage": "prod"}}, Context: map[string]any{"human_control": true, "service_actor_binding": taskexecution.DeclaredServiceBinding(), "execution_contract": map[string]any{"contract_version": "SECRETS-EXEC-20261011-v1", "operation": "metadata-apply-ccr", "execution_actor": "service:secrets-engine", "operation_deadline": now.Add(30 * time.Second).Format(time.RFC3339Nano)}, "approval": map[string]any{"caller_claim": "unchanged and untrusted in B1"}}}
}
func pilotEngine(t *testing.T) *decision.Engine {
t.Helper()
p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"))
if err != nil {
t.Fatal(err)
}
engine, err := decision.NewEngine(registry.NewStore(), p)
if err != nil {
t.Fatal(err)
}
return engine
}
func pilotToken(kind string, now time.Time) string {
payload := map[string]any{"case": kind, "exp": now.Add(2 * time.Minute).Unix()}
if kind == "expired" {
payload["exp"] = now.Add(-time.Second).Unix()
}
if kind == "no-expiry" {
delete(payload, "exp")
}
raw, _ := json.Marshal(payload)
return "synthetic." + base64.RawURLEncoding.EncodeToString(raw) + ".not-a-real-token"
}
func TestServicePilotRealHTTPAndTokenReviewSignedDeny(t *testing.T) {
now := time.Now().UTC()
var reviews atomic.Int64
reviewer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reviews.Add(1)
if r.URL.Path != "/apis/authentication.k8s.io/v1/tokenreviews" || r.Header.Get("Authorization") != "Bearer synthetic-reviewer" {
t.Error("unexpected reviewer route/auth")
w.WriteHeader(403)
return
}
var body struct {
Spec struct {
Token string `json:"token"`
Audiences []string `json:"audiences"`
} `json:"spec"`
}
if json.NewDecoder(r.Body).Decode(&body) != nil {
w.WriteHeader(400)
return
}
if !reflect.DeepEqual(body.Spec.Audiences, []string{"flex-auth", "access-engine"}) {
t.Error("unexpected bounded runtime audience request")
}
pieces := strings.Split(body.Spec.Token, ".")
raw, _ := base64.RawURLEncoding.DecodeString(pieces[1])
var claims map[string]any
json.Unmarshal(raw, &claims)
principal := taskexecution.ServicePilotPrincipal
audiences := []string{"flex-auth"}
authenticated := true
switch claims["case"] {
case "foreign":
principal = "system:serviceaccount:other:other"
case "alternate-audience":
audiences = []string{"access-engine"}
case "wrong-audience":
audiences = []string{"openbao"}
case "unauthenticated":
authenticated = false
}
json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"authenticated": authenticated, "audiences": audiences, "user": map[string]any{"username": principal}}})
}))
defer reviewer.Close()
folder := t.TempDir()
ca := filepath.Join(folder, "ca.crt")
reviewToken := filepath.Join(folder, "reviewer-token")
os.WriteFile(ca, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: reviewer.Certificate().Raw}), 0600)
os.WriteFile(reviewToken, []byte("synthetic-reviewer"), 0600)
engine := pilotEngine(t)
private := ed25519.NewKeyFromSeed(make([]byte, 32))
if engine.SetEnvelopeSigningKey(private, "synthetic-b1") != nil {
t.Fatal("signer config")
}
log := audit.NewJSONLDecisionLog(filepath.Join(folder, "decisions.jsonl"))
engine.SetDecisionLog(log)
bindings := map[string]string{"secrets-engine": taskexecution.ServicePilotPrincipal}
inputs, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "flex-auth", reviewer.URL, reviewToken, ca, bindings)
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(newServeMuxWithServicePilotInputs(engine, callerauth.Disabled(), inputs))
defer server.Close()
post := func(path string, request api.CheckRequest, token string) (int, []byte) {
raw, _ := json.Marshal(request)
req, _ := http.NewRequest("POST", server.URL+path, bytes.NewReader(raw))
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
response, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
body, _ := io.ReadAll(response.Body)
return response.StatusCode, body
}
original := pilotRequest(now)
status, body := post("/v1/check", original, pilotToken("valid", now))
if status != 200 {
t.Fatalf("status=%d", status)
}
var envelope api.DecisionEnvelope
if json.Unmarshal(body, &envelope) != nil {
t.Fatal("decode")
}
if envelope.Effect != api.DecisionEffectDeny || envelope.Reason != "service_execution_integration_unadmitted" {
t.Fatal("B1 changed policy gate")
}
if sign.Verify(envelope, map[string]ed25519.PublicKey{"synthetic-b1": private.Public().(ed25519.PublicKey)}) != nil {
t.Fatal("actual response signature")
}
if envelope.Binding.SubmittedRequestDigest != api.RequestDigest(original) || !reflect.DeepEqual(envelope.Binding.Context, original.Context) {
t.Fatal("original submitted request/context changed")
}
if envelope.Provenance.Caller == nil || envelope.Provenance.Caller.Mode != "enforce" || envelope.Provenance.Caller.Principal != taskexecution.ServicePilotPrincipal || envelope.Provenance.Caller.Audience != "flex-auth" {
t.Fatal("caller provenance")
}
records, err := log.ReadAll()
if err != nil || len(records) != 1 || !reflect.DeepEqual(records[0], envelope) {
t.Fatal("recorded signed response differs")
}
if reviews.Load() != 1 {
t.Fatal("request was reviewed more than once")
}
for _, kind := range []string{"foreign", "alternate-audience", "wrong-audience", "expired", "no-expiry", "unauthenticated"} {
t.Run(kind, func(t *testing.T) {
status, _ := post("/v1/check", pilotRequest(now), pilotToken(kind, now))
if status == 200 {
t.Fatal("invalid source caller accepted")
}
})
}
for _, mutate := range []func(*api.CheckRequest){func(r *api.CheckRequest) { r.Subject.ID = "other" }, func(r *api.CheckRequest) { r.Subject.Type = "human" }, func(r *api.CheckRequest) { r.Tenant = "other" }, func(r *api.CheckRequest) { r.Context["service_actor_binding"].(map[string]any)["extra"] = true }, func(r *api.CheckRequest) { r.Context["verified"] = true }, func(r *api.CheckRequest) { r.Context["delegation"] = true }, func(r *api.CheckRequest) {
r.Context["execution_contract"].(map[string]any)["operation_deadline"] = now.Add(5 * time.Minute).Format(time.RFC3339Nano)
}} {
request := pilotRequest(now)
mutate(&request)
if status, _ := post("/v1/check", request, pilotToken("valid", now)); status == 200 {
t.Fatal("invalid public request accepted")
}
}
if status, _ := post("/v1/check", pilotRequest(now), ""); status == 200 {
t.Fatal("missing caller accepted")
}
if status, _ := post("/v1/batch_check", pilotRequest(now), pilotToken("valid", now)); status != 403 {
t.Fatal("batch bypass")
}
records, _ = log.ReadAll()
if len(records) != 1 {
t.Fatal("refused request entered decision record")
}
// Exercise the actual production serve flag/configuration path in a disposable
// subprocess of this compiled test binary. Its environment contains only the
// synthetic argument handoff; no ambient backend credentials are inherited.
seedFile := filepath.Join(folder, "synthetic-seed")
os.WriteFile(seedFile, []byte(base64.RawURLEncoding.EncodeToString(make([]byte, 32))), 0440)
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
address := listener.Addr().String()
listener.Close()
registryPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json"))
policyPath, _ := filepath.Abs(filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"))
cliRecord := filepath.Join(folder, "cli-records.jsonl")
cliArgs := []string{"serve", "--addr", address, "--registry", registryPath, "--policy", policyPath, "--log", cliRecord, "--caller-auth-mode", "enforce", "--caller-audience", "flex-auth", "--caller-kubernetes-url", reviewer.URL, "--caller-reviewer-token-file", reviewToken, "--caller-ca-file", ca, "--caller-binding", "secrets-engine=" + taskexecution.ServicePilotPrincipal, "--envelope-signing-key-file", seedFile, "--envelope-signing-kid", "synthetic-b1"}
argsJSON, _ := json.Marshal(cliArgs)
child := exec.Command(os.Args[0], "-test.run=^TestServicePilotChildCLI$")
child.Env = []string{"ACCESS_B1_SERVER_ARGS=" + string(argsJSON)}
child.Stdout = io.Discard
child.Stderr = io.Discard
if child.Start() != nil {
t.Fatal("start disposable CLI")
}
defer func() { child.Process.Kill(); child.Wait() }()
ready := false
for attempt := 0; attempt < 100; attempt++ {
resp, err := http.Get("http://" + address + "/healthz")
if err == nil {
resp.Body.Close()
ready = true
break
}
time.Sleep(20 * time.Millisecond)
}
if !ready {
t.Fatal("disposable production serve path not ready")
}
raw, _ := json.Marshal(original)
cliRequest, _ := http.NewRequest("POST", "http://"+address+"/v1/check", bytes.NewReader(raw))
cliRequest.Header.Set("Authorization", "Bearer "+pilotToken("valid", now))
cliResponse, err := http.DefaultClient.Do(cliRequest)
if err != nil {
t.Fatal(err)
}
defer cliResponse.Body.Close()
var cliEnvelope api.DecisionEnvelope
if cliResponse.StatusCode != 200 || json.NewDecoder(cliResponse.Body).Decode(&cliEnvelope) != nil {
t.Fatal("production serve path refused supported source input")
}
if cliEnvelope.Effect != api.DecisionEffectDeny || cliEnvelope.Reason != "service_execution_integration_unadmitted" || sign.Verify(cliEnvelope, map[string]ed25519.PublicKey{"synthetic-b1": private.Public().(ed25519.PublicKey)}) != nil {
t.Fatal("production CLI did not emit signed DENY")
}
if cliEnvelope.Binding.SubmittedRequestDigest != api.RequestDigest(original) || !reflect.DeepEqual(cliEnvelope.Binding.Context, original.Context) {
t.Fatal("production CLI changed original request/context")
}
cliRecords, err := audit.NewJSONLDecisionLog(cliRecord).ReadAll()
if err != nil || len(cliRecords) != 1 || !reflect.DeepEqual(cliRecords[0], cliEnvelope) {
t.Fatal("production CLI response/record differ")
}
for _, mode := range []callerauth.Mode{callerauth.ModeWarn, callerauth.ModeDisabled} {
if _, err := buildServicePilotInputs(engine, mode, "flex-auth", reviewer.URL, reviewToken, ca, bindings); err == nil {
t.Fatal("non-enforcing task mode accepted")
}
}
if _, err := buildServicePilotInputs(engine, callerauth.ModeEnforce, "access-engine", reviewer.URL, reviewToken, ca, bindings); err == nil {
t.Fatal("task pin renamed audience accepted")
}
missing := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/v1/check", bytes.NewReader(body))
newServeMuxWithCallerAuth(engine, callerauth.Disabled()).ServeHTTP(missing, req)
if missing.Code != 503 {
t.Fatal("unwired generic handler accepted task pin")
}
}
func TestServicePilotGenericEngineAndCLIBypassesRefuse(t *testing.T) {
engine := pilotEngine(t)
request := pilotRequest(time.Now())
if _, err := engine.Check(context.Background(), request); err == nil {
t.Fatal("generic Check bypass")
}
if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{}); err == nil {
t.Fatal("generic Batch bypass")
}
if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{}); err == nil {
t.Fatal("generic List bypass")
}
if _, err := engine.CheckServicePilot(context.Background(), taskexecution.ServiceInput{}); err == nil {
t.Fatal("zero input trusted")
}
folder := t.TempDir()
path := filepath.Join(folder, "request.json")
raw, _ := json.Marshal(request)
os.WriteFile(path, raw, 0600)
var stdout, stderr bytes.Buffer
code := run([]string{"check", "--registry", filepath.Join("..", "..", "examples", "secrets-engine", "registry_snapshot.json"), "--policy", filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"), "--request", path}, &stdout, &stderr)
if code == 0 || stdout.Len() != 0 {
t.Fatal("generic CLI emitted task verdict")
}
}
// The earlier separately named positive fixture policy is not an exception:
// generic task actions cannot bypass the typed path on any loaded package.
func TestServicePilotSeparatelyNamedSyntheticAllowCannotBypass(t *testing.T) {
p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"))
if err != nil {
t.Fatal(err)
}
p.Metadata.ID = "synthetic-legacy-stack-positive-only"
p.RegoModule = "package " + p.Metadata.Package + "\ndefault decision := {\"effect\":\"allow\",\"reason\":\"synthetic_only\"}\n"
request := pilotRequest(time.Now())
result, err := p.Evaluate(context.Background(), request)
if err != nil || result.Effect != api.DecisionEffectAllow {
t.Fatal("synthetic precondition did not allow")
}
engine, err := decision.NewEngine(registry.NewStore(), p)
if err != nil {
t.Fatal(err)
}
if _, err := engine.Check(context.Background(), request); err == nil {
t.Fatal("separately named generic task allow escaped")
}
if _, err := engine.BatchCheck(context.Background(), api.BatchCheckRequest{Action: "execute-ccr"}); err == nil {
t.Fatal("generic task batch escaped")
}
if _, err := engine.ListAllowed(context.Background(), decision.ListAllowedRequest{Action: "execute-ccr"}); err == nil {
t.Fatal("generic task list escaped")
}
raw, _ := json.Marshal(request)
recorder := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/v1/check", bytes.NewReader(raw))
newServeMux(engine).ServeHTTP(recorder, req)
if recorder.Code != 503 {
t.Fatal("separate synthetic generic HTTP task route did not refuse")
}
}
func TestServicePilotWithholdsUnsignedOrAccidentalAllowBeforeRecord(t *testing.T) {
now := time.Now().UTC()
adapter, err := taskexecution.NewServicePilotInputs(fixedTokenReviewer{identity: callerauth.Identity{Username: taskexecution.ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(time.Minute)}})
if err != nil {
t.Fatal(err)
}
input, err := adapter.Prepare(context.Background(), "Bearer synthetic", pilotRequest(now), now)
if err != nil {
t.Fatal(err)
}
for _, accidentalAllow := range []bool{false, true} {
t.Run(map[bool]string{false: "unsigned", true: "accidental-allow"}[accidentalAllow], func(t *testing.T) {
p, err := policy.LoadAndValidateFile(context.Background(), filepath.Join("..", "..", "examples", "secrets-engine-service-task-execution", "policy_package.md"))
if err != nil {
t.Fatal(err)
}
if accidentalAllow {
p.RegoModule = "package " + p.Metadata.Package + "\ndefault decision := {\"effect\":\"allow\",\"reason\":\"synthetic_only\"}\n"
}
engine, err := decision.NewEngine(registry.NewStore(), p)
if err != nil {
t.Fatal(err)
}
if accidentalAllow {
engine.SetEnvelopeSigningKey(ed25519.NewKeyFromSeed(make([]byte, 32)), "synthetic")
}
log := audit.NewJSONLDecisionLog(filepath.Join(t.TempDir(), "records.jsonl"))
engine.SetDecisionLog(log)
if _, err := engine.CheckServicePilot(context.Background(), input); err == nil {
t.Fatal("B1 released unsupported result")
}
records, err := log.ReadAll()
if err != nil || len(records) != 0 {
t.Fatal("withheld result entered recorder")
}
})
}
}
func TestServicePilotChildCLI(t *testing.T) {
raw := os.Getenv("ACCESS_B1_SERVER_ARGS")
if raw == "" {
return
}
var args []string
if json.Unmarshal([]byte(raw), &args) != nil {
os.Exit(2)
}
os.Exit(run(args, os.Stdout, os.Stderr))
}

View file

@ -51,9 +51,32 @@ trusted served approval, consume confirmation or operational entitlement.
Maker tests use a synthetic TokenReviewer, not actual Kubernetes authentication.
Actual deployed TokenReview origin/CA/RBAC/audience/projection rotation and mapping
admission must be proved before runtime use. The adapter is not wired into HTTP
routes, decision engine evaluation or a native executor; source package remains
hard-closed. A forged public service_actor_binding alone remains insufficient.
admission must be proved before runtime use. The B1 source path now wires the actual service adapter into `/v1/check` only
when the exact installed service-pilot v1 package is selected. Startup requires
enforced retained `flex-auth` audience and the one exact Secrets mapping. The
actual reviewer authenticates once; the detached ServiceInput is rechecked for
caller/operation expiry at use. The single existing decision engine records only
a signed DENY on this path; unsigned output or an accidental policy allow is
withheld before recording. No native executor or Approval fact join is installed.
A forged public service_actor_binding alone remains insufficient.
Generic Check, BatchCheck, ListAllowed and CLI/HTTP evaluation cannot evaluate
`execute-ccr` under any package; the dedicated typed path is required. The exact
service pin also refuses generic calls and batch requests. This deliberately
invalidates the older separately named synthetic positive stack fixture on the
new source. Its positive proof remains historical at Access `ec21518e`, not proof
for B1. Do not add a fixture exception; a positive successor must use reviewed
B2/B3 provider joins. The literal service policy false gate is unchanged.
B1 retains the exact original request context, carried claim, submitted digest
and producer-owned canonicalization. Its bounded strict JSON decoder refuses
duplicates, trailing/unknown typed fields and oversized input. The caller's
Approval blob remains unverified and cannot enable anything. B2 requires the
Approval owner's fact contract/vector review; B3 additionally requires exact
server-owned native-act representation and the complete trusted join. Fresh
provider observations stay out of band: no refreshed context or claim replacement
may relax strict Secrets request correspondence. Access never invokes a
credential-carrying Python subprocess, consumes approval or performs the effect.
## Exact operation and independent gates
@ -129,7 +152,8 @@ bind actual compiler bytes to the whole descriptor, enforce journal phase/order,
admit grant/baseline/estate or perform an effect. Those are explicit implementation
requirements; a matching predicate and reviewed TokenReview input are insufficient.
The service adapter carries authenticated transport evidence outside the canonical
request but is not connected to HTTP evaluation/provider validation. A caller's
request. B1 connects it only to signed-DENY HTTP evaluation; provider validation and
executable eligibility remain absent. A caller's
service_actor_binding remains a public declaration independently compared by the
adapter, never a substitute for runtime provenance or exact operation authority.

View file

@ -20,6 +20,7 @@ import (
"github.com/netkingdom/access-engine/internal/policy"
"github.com/netkingdom/access-engine/internal/registry"
"github.com/netkingdom/access-engine/internal/sign"
"github.com/netkingdom/access-engine/internal/taskexecution"
"github.com/netkingdom/access-engine/pkg/api"
)
@ -153,9 +154,44 @@ func (e *Engine) decide(ctx context.Context, request api.CheckRequest) (api.Deci
return envelope, nil
}
// IsServicePilot selects the existing exact source package, never a request flag.
func (e *Engine) IsServicePilot() bool {
return e.policy.Metadata.ID == taskexecution.ServicePilotPackageID && e.policy.Metadata.Version == taskexecution.ServicePilotPackageVersion
}
// ErrServicePilotUnavailable withholds any result that is not this B1 slice's
// signed DENY. Provider joins and executable eligibility remain unimplemented.
var ErrServicePilotUnavailable = errors.New("service pilot source path unavailable")
// CheckServicePilot records only a signed DENY after authenticating the exact
// immutable service input. It does not validate Approval facts or grant an act.
func (e *Engine) CheckServicePilot(ctx context.Context, input taskexecution.ServiceInput) (api.DecisionEnvelope, error) {
if !e.IsServicePilot() {
return api.DecisionEnvelope{}, ErrServicePilotUnavailable
}
request, err := input.RequestAt(e.now())
if err != nil {
return api.DecisionEnvelope{}, err
}
envelope, err := e.decide(callerauth.WithRecord(ctx, input.Caller()), request)
if err != nil {
return api.DecisionEnvelope{}, err
}
if envelope.Effect != api.DecisionEffectDeny || envelope.Signature == nil || envelope.Signature.Mode != api.EnvelopeSignatureSigned {
return api.DecisionEnvelope{}, ErrServicePilotUnavailable
}
if err := e.recordDecisions([]api.DecisionEnvelope{envelope}); err != nil {
return api.DecisionEnvelope{}, err
}
return envelope, nil
}
// Check evaluates one subject/action/resource request. The decision is
// released only after its required record commits (GH-DEC-2026-024).
func (e *Engine) Check(ctx context.Context, request api.CheckRequest) (api.DecisionEnvelope, error) {
if e.IsServicePilot() || request.Action == "execute-ccr" {
return api.DecisionEnvelope{}, ErrServicePilotUnavailable
}
decision, err := e.decide(ctx, request)
if err != nil {
return api.DecisionEnvelope{}, err
@ -169,6 +205,9 @@ func (e *Engine) Check(ctx context.Context, request api.CheckRequest) (api.Decis
// BatchCheck evaluates one subject/action/context tuple against resources in
// request order, and commits the whole batch in one durable write.
func (e *Engine) BatchCheck(ctx context.Context, request api.BatchCheckRequest) ([]api.DecisionEnvelope, error) {
if e.IsServicePilot() || request.Action == "execute-ccr" {
return nil, ErrServicePilotUnavailable
}
decisions := make([]api.DecisionEnvelope, 0, len(request.Resources))
for _, resource := range request.Resources {
decision, err := e.decide(ctx, api.CheckRequest{
@ -193,6 +232,9 @@ func (e *Engine) BatchCheck(ctx context.Context, request api.BatchCheckRequest)
// ListAllowed evaluates candidate resources and returns only allow decisions.
func (e *Engine) ListAllowed(ctx context.Context, request ListAllowedRequest) ([]api.DecisionEnvelope, error) {
if e.IsServicePilot() || request.Action == "execute-ccr" {
return nil, ErrServicePilotUnavailable
}
candidates := e.store.ResourceRefs(request.System, request.ResourceType)
decisions := make([]api.DecisionEnvelope, 0, len(candidates))
for _, resource := range candidates {

View file

@ -1,5 +1,6 @@
// Package taskexecution contains proposed source-only identity input validation.
// It is not registered with HTTP evaluation and grants no executable admission.
// Package taskexecution contains task input validation, not policy evaluation.
// The human component remains source-only. The B1 service HTTP path can emit
// only signed DENY and grants no executable admission.
package taskexecution
import (

View file

@ -1,20 +1,27 @@
package taskexecution
import (
"bytes"
"context"
"encoding/json"
"errors"
"github.com/netkingdom/access-engine/internal/callerauth"
"github.com/netkingdom/access-engine/pkg/api"
"io"
"time"
)
var ErrServiceInput = errors.New("service task execution input refused")
const ServicePilotPackageID = "secrets-engine.task-execution.service-pilot"
const ServicePilotPackageVersion = "v1"
const ServicePilotMappingVersion = "proposed-service-task-caller/v1"
const ServicePilotPrincipal = "system:serviceaccount:secrets-engine:secrets-engine"
// ServicePilotInputs is source preparation only. It installs the explicit
// ServicePilotInputs installs the exact B1 service input mapping for signed DENY.
// It does not validate Approval facts or establish executable eligibility.
// It installs the explicit
// proposed fixed subject-to-transport mapping, never a request-selected mapping.
// No constructor parameter can select warn/disabled mode or another subject.
type ServicePilotInputs struct{ auth *callerauth.Authenticator }
@ -47,6 +54,44 @@ func (s ServiceInput) Request() (api.CheckRequest, error) {
}
func (s ServiceInput) Caller() callerauth.Record { return s.caller }
// RequestAt rechecks transport and operation lifetime at the runtime use site.
// Request() remains a detached source snapshot, never an execution capability.
func (s ServiceInput) RequestAt(now time.Time) (api.CheckRequest, error) {
r, err := s.Request()
if err != nil || now.IsZero() || s.caller.Audience != "flex-auth" || !s.caller.NotAfter.After(now) {
return api.CheckRequest{}, ErrServiceInput
}
contract, ok := r.Context["execution_contract"].(map[string]any)
if !ok {
return api.CheckRequest{}, ErrServiceInput
}
value, ok := contract["operation_deadline"].(string)
if !ok {
return api.CheckRequest{}, ErrServiceInput
}
deadline, err := time.Parse(time.RFC3339Nano, value)
if err != nil || !deadline.After(now) || deadline.After(s.caller.NotAfter) {
return api.CheckRequest{}, ErrServiceInput
}
return r, nil
}
// DecodeServiceRequest reuses the owner strict JSON scanner for this bounded
// task-only route. It never changes producer canonicalization or legacy input.
func DecodeServiceRequest(reader io.Reader) (api.CheckRequest, error) {
raw, err := io.ReadAll(io.LimitReader(reader, 524289))
var request api.CheckRequest
if err != nil || len(raw) > 524288 || strictObject(raw, &request) != nil {
return request, ErrServiceInput
}
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
if decoder.Decode(&request) != nil {
return api.CheckRequest{}, ErrServiceInput
}
return request, nil
}
// DeclaredServiceBinding is public immutable metadata to bind at approval issue.
// It is not authentication evidence; Prepare independently verifies its principal.
func DeclaredServiceBinding() map[string]any {

View file

@ -1,7 +1,9 @@
package taskexecution
import (
"bytes"
"context"
"encoding/json"
"github.com/netkingdom/access-engine/internal/callerauth"
"github.com/netkingdom/access-engine/pkg/api"
"testing"
@ -79,3 +81,33 @@ func TestServicePilotRefusals(t *testing.T) {
t.Fatal("missing owner reviewer accepted")
}
}
func TestServiceInputRuntimeRecheckAndStrictHTTPDecode(t *testing.T) {
now := time.Now().UTC()
r := serviceRequest(t, now)
reviewer := &serviceReviewer{identity: callerauth.Identity{Username: ServicePilotPrincipal, Audiences: []string{"flex-auth"}, NotAfter: now.Add(120 * time.Second)}}
adapter, _ := NewServicePilotInputs(reviewer)
input, err := adapter.Prepare(context.Background(), "Bearer synthetic", r, now)
if err != nil {
t.Fatal(err)
}
if _, err := input.RequestAt(now); err != nil {
t.Fatal(err)
}
for _, when := range []time.Time{time.Time{}, now.Add(60 * time.Second), now.Add(120 * time.Second)} {
if _, err := input.RequestAt(when); err == nil {
t.Fatal("stale/invalid runtime input accepted")
}
}
raw, _ := json.Marshal(r)
decoded, err := DecodeServiceRequest(bytes.NewReader(raw))
if err != nil || api.RequestDigest(decoded) != api.RequestDigest(r) {
t.Fatal("valid decode changed canonical submitted input")
}
bad := [][]byte{append(append([]byte{}, raw...), []byte(" {}")...), []byte(`{"action":"execute-ccr","action":"apply"}`), []byte(`{"context":{"human_control":true,"human_control":false}}`), []byte(`{"unknown_task_flag":true}`), bytes.Repeat([]byte(" "), 524289)}
for _, body := range bad {
if _, err := DecodeServiceRequest(bytes.NewReader(body)); err == nil {
t.Fatal("malformed/oversized task request accepted")
}
}
}

View file

@ -170,6 +170,7 @@ that observation applied to the artifact the standard regulates.
id: FLEX-WP-0024-T05
status: done
priority: high
state_hub_task_id: "cfe4d275-9f02-5090-8391-06eea59d45c5"
```
2026-10-16 source follow-through: NetKingdom target Secrets Engine R2 owner review
@ -186,6 +187,7 @@ This is source preparation only, not signer admission or deployed acceptance.
id: FLEX-WP-0024-T06
status: progress
priority: high
state_hub_task_id: "7f785b52-bf62-5884-8d34-670d70bc9023"
```
Wait on Platform/OpenBao key custody, independent public-key and issuer/policy-pin
@ -226,3 +228,14 @@ revision, and adds offline native signer receipt verification using the existing
Secrets verifier. No key, trust, image publication or live activation is admitted.
The managed workload's missing rapp declaration remains explicit migration debt;
this source binding does not fabricate a package declaration or raise its grade.
2026-10-16 separate B1 source follow-through wires ServicePilotInputs through actual
HTTP/TokenReview into a signed-DENY-only engine entry. Generic `execute-ccr` calls
refuse across every policy, including the old separately named positive fixture;
that proof stays pinned to ec21518e. Original context/submitted digest stay unchanged.
Actual disposable serve/TLS TokenReview/HTTP signed-DENY and bypass/refusal tests are
source evidence only. B2 depends on independently reviewed Approval fact contract
and canonical vectors; B3 additionally needs exact source/normalization numeric
representation and full trusted provider joins. No current policy false gate,
production trust, native deployment or protected effect is enabled by B1.