1-SEC contributors · Reviewed · Module: iot_shield

Deny-by-Default Management Access with a Trusted Gateway

The authenticated management-access endpoint checks a configured source policy and exact verified operator identity.

What 1-SEC checks

The authenticated management-access endpoint checks a configured source policy and exact verified operator identity. A trusted gateway must call it before forwarding. The API requires write scope; missing/disabled or uninitialized policy does not create an allow decision.

Read the implementation: handleManagementAccessCheck

Required telemetry and context

This control uses an explicit configuration/API path rather than a standalone detector input event. Verify the caller and configuration before relying on its result.

  • source_ip
  • operator
  • identity_verified (boolean)
  • target
  • action
  • management_access.allowed_sources
  • management_access.allowed_operators

Field names above describe relevant context, not a complete API request. Consult the current event contract for envelope and detail requirements. Preserve booleans, numbers, and strings as the receiving handler expects them. Agent-provided text cannot independently verify identity or authorization.

Deployment, prevention, and response

Remove bypass paths. Forward only on HTTP 200 with allowed: true; deny on a denied, failed, timed-out, or malformed decision. Test both a permitted request and a rejected request against a controlled backend, then verify failure does not forward. Do not reuse a prior decision as a session grant.

Record the source event, affected agent/host, actual target, policy decision, and response result. Review enforcement configuration separately from detection. A recommendation, selected action, dry-run record, and completed action are different outcomes.

Reproduce the source fixtures

From a checkout of the linked 1-SEC source revision, run the selected fixtures with the repository's Go toolchain. These commands run tests; they do not execute a real attack against your systems.

go test ./internal/api -run '^(TestManagementAccessCheckHTTP|TestManagementAccessUnavailableDenies|TestGatewayStopsAccessBeforeBackend)$' -count=1

The referenced source fixtures ran in the selected local package suites. Helper tests and synthetic events establish only the assertions shown, not production prevention or framework compatibility.

TestManagementAccessCheckHTTP

Full fixture and assertions — the excerpt below is truncated; run the original test.

func TestManagementAccessCheckHTTP(t *testing.T) {
	for _, tc := range []struct {
		name, body, key, method string
		status                  int
	}{
		{"allow", accessRequestJSON, "test-key", "POST", 200},
		{"outside network", strings.Replace(accessRequestJSON, "10.20.0.7", "198.51.100.23", 1), "test-key", "POST", 403},
		{"unverified", strings.Replace(accessRequestJSON, "true", "false", 1), "test-key", "POST", 403},
		{"identity string", strings.Replace(accessRequestJSON, "true", `"true"`, 1), "test-key", "POST", 400},
		{"unknown operator", strings.Replace(accessRequestJSON, "engineer", "attacker", 1), "test-key", "POST", 403},
		{"missing context", `{}`, "test-key", "POST", 403},
		{"null", `null`, "test-key", "POST", 403},
		{"unknown field", strings.Replace(accessRequestJSON, `"action":`, `"admin":true,"action":`, 1), "test-key", "POST", 400},
		{"trailing JSON", accessRequestJSON + ` {}`, "test-key", "POST", 400},
		{"oversized", accessRequestJSON + strings.Repeat(" ", 16<<10), "test-key", "POST", 400},
		{"missing API key", accessRequestJSON, "", "POST", 401},
		{"read only API key", accessRequestJSON, "reader-key", "POST", 403},
		{"wrong API key", accessRequestJSON, "attacker-key", "POST", 403},
		{"wrong method", accessRequestJSON, "test-key", "GET", 405},
	} {
		t.Run(tc.name, func(t *testing.T) {
			s := accessTestServer(t)
			s.engine.Config.Server.ReadOnlyKeys = []string{"reader-key"}
			req := httptest.NewRequest(tc.method, "/api/v1/access/check", strings.NewReader(tc.body))
			req.Header.Set("X-Forwarded-For", "10.20.0.7")
			if tc.key != "" {
				req.Header.Set("Authorization", "Bearer "+tc.key)
			}
			w := httptest.NewRecorder()
			s.server.Handler.ServeHTTP(w, req)
			if w.Code != tc.status {
				t.Fatalf("got %d %s, want %d", w.Code, w.Body.String(), tc.status)
			}
			if tc.status == 200 || tc.status == 403 && tc.key == "test-key" {
				var d core.ManagementAccessDecision
				if err := json.Unmarshal(w.Body.Bytes(), &d); err != nil {
					t.Fatal(err)
				}
				if d.Allowed != (tc.status == 200) {
					t.Fatalf("wrong decision %+v", d)
				}
			}
TestManagementAccessUnavailableDenies

Full fixture and assertions — run in its original package with its helpers.

func TestManagementAccessUnavailableDenies(t *testing.T) {
	for _, state := range []string{"missing module", "disabled module", "not started", "no policy", "no API keys"} {
		t.Run(state, func(t *testing.T) {
			s := accessTestServer(t)
			want := 503
			switch state {
			case "missing module":
				s = newTestServer(testEngine())
			case "disabled module":
				s.engine.Config.Modules[iot.ModuleName] = core.ModuleConfig{Enabled: false}
			case "not started":
				e := testEngine()
				_ = e.Registry.Register(iot.New())
				s = newTestServer(e)
				want = 403
			case "no policy":
				e := testEngine()
				shield := iot.New()
				_ = shield.Start(context.Background(), nil, e.Pipeline, e.Config)
				t.Cleanup(func() { _ = shield.Stop() })
				_ = e.Registry.Register(shield)
				s = newTestServer(e)
				want = 403
			case "no API keys":
				s.engine.Config.Server.APIKeys = nil
				want = 403
			}
			w := httptest.NewRecorder()
			s.server.Handler.ServeHTTP(w, authedReq(httptest.NewRequest("POST", "/api/v1/access/check", strings.NewReader(accessRequestJSON))))
			if w.Code != want {
				t.Fatalf("got %d, want %d", w.Code, want)
			}
		})
	}
}

// A simulated gateway must obtain an explicit allow before touching its backend.
TestGatewayStopsAccessBeforeBackend

Full fixture and assertions — the excerpt below is truncated; run the original test.

func TestGatewayStopsAccessBeforeBackend(t *testing.T) {
	s := accessTestServer(t)
	policyAPI := httptest.NewServer(s.server.Handler)
	defer policyAPI.Close()
	forwarded := 0
	gateway := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ip, _, err := net.SplitHostPort(r.RemoteAddr)
		if err != nil {
			http.Error(w, "invalid client address", 400)
			return
		}
		// A real gateway validates an identity-provider session. This fixture
		// models that result independently of client claims and device login.
		verified := r.Header.Get("Authorization") == "Bearer verified-session"
		check := core.ManagementAccessRequest{SourceIP: ip, Operator: "engineer", IdentityVerified: verified, Target: "device-17", Action: "configure"}
		body, err := json.Marshal(check)
		if err != nil {
			t.Fatal(err)
		}
		req, err := http.NewRequestWithContext(r.Context(), "POST", policyAPI.URL+"/api/v1/access/check", bytes.NewReader(body))
		if err != nil {
			t.Fatal(err)
		}
		req.Header.Set("Authorization", "Bearer test-key")
		res, err := policyAPI.Client().Do(req)
		if err != nil {
			http.Error(w, "access check unavailable", http.StatusServiceUnavailable)
			return
		}
		defer res.Body.Close()
		var decision core.ManagementAccessDecision
		if err := json.NewDecoder(io.LimitReader(res.Body, 16<<10)).Decode(&decision); err != nil || res.StatusCode != 200 || !decision.Allowed {
			http.Error(w, "access denied", http.StatusForbidden)
			return
		}
		forwarded++
		w.WriteHeader(http.StatusNoContent)
	})
	for _, tc := range []struct {
		name, ip, session         string
		wantStatus, wantForwarded int
	}{

Tuning, evaluation, and coverage limits

The gateway independently authenticates the operator and supplies the verification context. The endpoint does not verify arbitrary identity tokens or implement a per-resource authorization model. A direct route around the gateway remains exposed regardless of the decision service.

Evaluate this behavior with representative authorized activity and a controlled known-positive case. Investigate the exact rule, inputs, and state before suppressing an alert. Do not assume a hard-coded heuristic is a configurable setting. Measure your own deployment; the source fixtures do not establish a benchmark, detection rate, compliance certification, or incident-prevention claim.

Implementation reviewed at source revision 2af8ea9dab2057a1ffc3cce9bf44220271fab18e. Recheck the actual release and integration before deployment. Sources and corrections policy.