This milestone is the explicit completion gate for Phase 1. It exists because individual milestone acceptance criteria only verify their own narrow slice. A system can have all milestones marked complete and still have emergent security failures when the pieces are composed together. What individual milestone tests c
Milestone 1.5.1 — Phase 1 Security Integration Test Suite (Cross-Tenant + Auth Boundary Gate)
Status: Complete
Goal: 1.5 — Phase 1 Security Gate
Phase: 1 — Core Platform
Estimated effort: 3–4 days
ADR required: None (test suite, not design decision)
Why This Milestone Exists
This milestone is the explicit completion gate for Phase 1. It exists because individual milestone acceptance criteria only verify their own narrow slice. A system can have all milestones marked complete and still have emergent security failures when the pieces are composed together.
What individual milestone tests cannot catch:
- M1.2.1 tests that auth middleware calls gRPC correctly.
- M1.2.5 tests that agent verification calls auth correctly.
- M1.2.4 tests that rate limiting calls Redis correctly.
- None of them test that the composed middleware chain — auth → agent verification → rate limit → handler — enforces the full security model end-to-end through a real HTTP connection with a real Postgres database and real Redis.
This milestone writes and runs exactly those end-to-end security integration tests. It is the Phase 1 completion certificate.
The security model being validated:
- Every request without a valid token is rejected before reaching the handler.
- Every request with a valid token for Org A cannot access resources belonging to Org B.
- Revoked tokens are rejected within the documented SLA.
- Agents belonging to a different org are rejected, even with a valid token.
- Rate limits are enforced per org and return correct headers.
- Permission bitmap enforcement rejects insufficient permissions.
Non-Goals
- Performance or load testing (covered in Phase 2 milestone 2.6.1)
- Fuzzing or penetration testing (out of scope for Phase 1)
- Testing Phase 2+ functionality (LLM forwarding, directive injection)
Branch
test/m1-5-1-security-integration-test-suite
PR Title
test(security): Phase 1 security integration test suite — Phase 1 gate (m1.5.1)
Prerequisites
All of the following must be merged before this milestone begins:
- 1.1.7 — users + agents tables
- 1.2.4 — rate limiting middleware
- 1.2.5 — agent identity verification
- 1.2.6 — request ID middleware
- 1.4.1 — seed data and compose-test stack
Test Matrix
Category 1: Token Authentication
| Test ID | Scenario | Input | Expected |
|---|---|---|---|
| SEC-1.1 | No Authorization header | — | 401 MISSING_TOKEN |
| SEC-1.2 | Authorization: Bearer with empty value | "" | 401 MISSING_TOKEN |
| SEC-1.3 | Malformed token (not parseable) | "not_a_token" | 401 |
| SEC-1.4 | Valid format, token not in DB | "ibex_sk_unknowntoken" | 401 |
| SEC-1.5 | Valid token, revoked | Revoke via API, then use | 401 within revocation SLA (default 300ms; override REVOCATION_SLA_MS) |
| SEC-1.6 | Valid token, expired | Token with expires_at = past | 401 |
| SEC-1.7 | Valid token, active | Seed token | Continues to next middleware |
Category 2: Agent Identity Verification
| Test ID | Scenario | Input | Expected |
|---|---|---|---|
| SEC-2.1 | Missing X-IBEX-Agent-ID header | — | 400 MISSING_AGENT_ID |
| SEC-2.2 | X-IBEX-Agent-ID not a valid UUID | "not-a-uuid" | 400 |
| SEC-2.3 | X-IBEX-Agent-ID is a valid UUID not in DB | Random UUID | 403 AGENT_NOT_AUTHORIZED |
| SEC-2.4 | X-IBEX-Agent-ID belongs to Org B, token from Org A | Cross-org | 403 AGENT_NOT_AUTHORIZED |
| SEC-2.5 | Agent is paused | Agent with status="paused" | 403 AGENT_SUSPENDED |
| SEC-2.6 | Agent is archived | Agent with status="archived" | 403 AGENT_SUSPENDED |
| SEC-2.6b | Agent is suspended | Agent with status="suspended" | 403 AGENT_SUSPENDED |
| SEC-2.7 | Valid token + valid agent for same org | Seed data | Continues to handler |
| SEC-2.8 | Valid token + agent, auth gRPC unavailable | Close auth server | 503 SERVICE_DEGRADED + envelope |
Category 3: Cross-Tenant Isolation via Proxy
| Test ID | Scenario | Expected |
|---|---|---|
| SEC-3.1 | Org A token + Org A agent → request proceeds past auth | 200 (no LLM) or 501 (no provider) |
| SEC-3.2 | Org A token + Org B agent → rejected | 403 (not 404) |
| SEC-3.3 | Org B token + Org A agent → rejected | 403 (not 404) |
| SEC-3.4 | Response to SEC-3.2 does NOT differ in timing from SEC-3.3 | p95 timing delta < 50ms in CI (local target 5ms) |
Category 4: Rate Limiting
| Test ID | Scenario | Expected |
|---|---|---|
| SEC-4.1 | Requests under limit | X-RateLimit-Remaining decrements correctly |
| SEC-4.2 | 61st request with limit=60 | 429 RATE_LIMITED |
| SEC-4.3 | 429 response has Retry-After header | Value ≤ 60, Value > 0 |
| SEC-4.4 | 429 response has X-RateLimit-Reset header | Unix timestamp within 60s of now |
| SEC-4.5 | Org A's rate limit does not affect Org B | Fire 61 reqs from Org A; Org B still gets 200 |
| SEC-4.6 | Redis unavailable (closed miniredis) | Request succeeds — fail-open per ADR-0015 |
Category 5: Permission Bitmap Enforcement
| Test ID | Scenario | Expected |
|---|---|---|
| SEC-5.1 | Token with no permissions (permissions=0) | 403 INSUFFICIENT_PERMISSIONS |
| SEC-5.2 | Token with read-only permission on write endpoint | 403 |
| SEC-5.3 | Token with full permissions | Request proceeds |
Category 6: Response Headers and Envelope Consistency
| Test ID | Scenario | Expected |
|---|---|---|
| SEC-6.1 | All error responses | Have Content-Type: application/json |
| SEC-6.2 | All error responses | JSON body matches ErrorResponse schema |
| SEC-6.3 | All error responses | request_id field populated and matches X-Request-ID header |
| SEC-6.4 | All 4xx/5xx | X-Request-ID header present |
| SEC-6.5 | All responses | No Authorization or token value appears in response body |
Test Infrastructure
//go:build integration
// Test infrastructure pattern for security tests:
// Each test creates its own orgs and data and runs in isolation.
type securityTestEnv struct {
proxy *httptest.Server // proxy under test (real middleware chain)
auth *grpc.Server // real auth gRPC service
postgres *pgxpool.Pool // test Postgres via testcontainers
redis *miniredis.Miniredis
}
// setupTwoOrgs creates two isolated orgs with agents and tokens.
// All data is cleaned up by t.Cleanup.
func setupTwoOrgs(t *testing.T, env *securityTestEnv) (orgA, orgB testOrgContext)
type testOrgContext struct {
OrgID uuid.UUID
AgentID uuid.UUID
Token string // raw PAT
}Acceptance Criteria
- All 35+ test cases in the matrix above pass with
-tags=integration - SEC-3.4: p95 timing delta between cross-org denials < 50ms in CI
- SEC-4.5: rate limits are per-org (Org B not affected by Org A's exhaustion)
- All error responses pass SEC-6.x schema checks
- Test suite runs in < 60 seconds in CI
- CI gate
security-integrationadded to required checks onmainbranch -
docs/SECURITY.mdupdated with the security model that this suite validates
Risks
| Risk | Likelihood | Mitigation |
|---|---|---|
| SEC-3.4 timing test is flaky in slow CI environments | Medium | Use relative delta (p95 of 100 measurements), not absolute threshold |
| Miniredis does not perfectly replicate Redis INCR + EXPIRE atomicity | Low | Use real Redis container for SEC-4.x tests |
| Test suite becomes a maintenance burden | Low | Tests use shared securityTestEnv; adding new scenarios is one table row |
Last updated on