Multi-Tenant Policy Rollout Flow
Implementation status (reconciled against
src/Policy). The cross-tenant, percentage-staged, auto-canary rollout orchestration described in this document is NOT IMPLEMENTED as a single feature. There is no rollout engine, no impact-analysis pre-flight, no percentage/scan-volume tenant selection, no automatic-rollback trigger evaluation, and no blue-green / canary-traffic / feature-flag strategy executor anywhere in the Policy module. The JSON/YAML rollout-plan, impact-analysis, and stage-metrics payloads below are forward-looking design drafts, not contracts emitted by any running service.What does exist today (verified in source) and composes most of this story:
- Policy Engine HTTP surface for the per-pack lifecycle:
POST /api/policy/packs(create pack),POST .../revisions(Draft or Approved),POST .../revisions/{v}/bundle(compile + sign),POST .../revisions/{v}/evaluate, andPOST .../revisions/{v}:activatewith two-person-approval enforcement. Revision lifecycle states are exactlyDraft -> Approved -> Active(StellaOps.Policy.Engine/Domain/PolicyPackRecord.cs).- Policy Registry library (
StellaOps.Policy.Registry, no HTTP host of its own — consumed via the CLI GitOps path and the typedPolicyRegistryClient): environment promotion bindings (Dev/Stage/Prod) with modesManual,AutomaticOnApproval,Scheduled,Canary; rollback by binding/steps; promotion history; publish pipeline (signing/attestation); and a review workflow (submit / approve / reject / request-changes). The onlyIPolicyPackStoreimplementation shipped in the library is the test-harnessInMemoryPolicyPackStore; production registration is store-agnostic (AddPolicyPackStore<TStore>) but no durable (e.g. Postgres) store exists in this repo, so in practice the Registry is in-memory only.Sections that map onto real code are tagged [IMPLEMENTED]; sections that describe unbuilt orchestration are tagged [DRAFT / NOT IMPLEMENTED].
Overview
The Multi-Tenant Policy Rollout Flow describes how StellaOps intends to propagate policy changes across multiple tenants in a controlled, auditable manner. The target design supports staged rollouts, canary deployments, and rollback capabilities for enterprise policy governance.
Note on terminology: today the building blocks are per-pack revisions and per-environment promotion bindings (Dev -> Stage -> Prod), not percentage-staged multi-tenant cohorts. “Promotion” in the codebase moves one pack version into one environment for one tenant; it is not a fan-out across a tenant population.
Audience: policy administrators and platform engineers managing policy-pack lifecycle and environment promotion. Read the implementation-status banner above first — the staged multi-tenant rollout orchestration is forward-looking design; the live surface is the Implemented Lifecycle.
Business Value: Centralized policy management with controlled rollout reduces risk of policy changes breaking production workflows while ensuring consistent security standards across the organization.
Actors
| Actor | Type | Role | Status |
|---|---|---|---|
| Policy Admin | Human | Authors/approves/activates policy revisions (policy:author, policy:review, policy:approve, policy:activate) | [IMPLEMENTED] |
| Platform Admin | Human | Manages tenant catalog (authority:tenants.read/.write); would own cross-tenant rollouts | partial |
| Policy Engine | Service | Compiles, evaluates, and activates policy pack revisions | [IMPLEMENTED] |
| Authority | Service | Issues OpToks and owns the flat tenant catalog | [IMPLEMENTED] |
| Notify | Service | Receives policy.profile.* notification events | [IMPLEMENTED] |
| Scheduler | Service | Would orchestrate staged rollout | [NOT IMPLEMENTED for this flow] |
Tenant hierarchy / inheritance — NOT IMPLEMENTED. There is no organization -> team -> project tenant tree in Authority and no policy
inherits_fromresolution anywhere insrc/. The tenant model is flat: a tenant is an opaque identifier carried in the envelope-boundstellaops:tenantclaim. The Authority “tenant hierarchy” actor role and theScheduler“Get tenant hierarchy” call in the diagram below are aspirational. Policy notification events do carry aneffective_scopeblock (tenants/projects/purl_patterns/cpe_patterns/tags— seeNotifications/PolicyProfileNotificationModels.cs), but that is a flat scope filter, not an inheritance hierarchy.
Prerequisites
- Multi-tenant environment configured (flat tenant catalog in Authority)
- Caller holds the relevant
policy:*scopes (see Authorization Scopes)
[DRAFT / NOT IMPLEMENTED] The original prerequisites below assume features that do not exist yet:
Tenant hierarchy defined (org → teams → projects)— no hierarchy exists.Policy inheritance rules established— noinherits_fromresolution exists.Rollout approval workflow configured— there is a per-revision two-person activation workflow and a per-pack review workflow, but no rollout-level approval object.
Tenant Hierarchy
[DRAFT / NOT IMPLEMENTED] — illustrative only; Authority stores a flat tenant list, not the tree shown here.
Organization (acme-corp)
├── Team: Platform Engineering
│ ├── Project: core-services
│ └── Project: infrastructure
├── Team: Product Development
│ ├── Project: web-app
│ ├── Project: mobile-api
│ └── Project: data-pipeline
└── Team: Security
└── Project: security-tools
Flow Diagram
[DRAFT / NOT IMPLEMENTED] — the staged-rollout choreography (Scheduler-driven tenant-tree fetch, canary stage, 25%/100% cohorts, monitor windows) is a target design. No service emits or consumes these messages today. For the implemented sequence see Implemented Lifecycle below.
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Multi-Tenant Policy Rollout Flow (TARGET DESIGN) │
└─────────────────────────────────────────────────────────────────────────────────┘
┌──────────┐ ┌─────────┐ ┌───────────┐ ┌──────────┐ ┌────────┐ ┌────────┐
│ Policy │ │ Policy │ │ Scheduler │ │ Authority│ │ Policy │ │ Notify │
│ Admin │ │ Store │ │ │ │ │ │ Engine │ │ │
└────┬─────┘ └────┬────┘ └─────┬─────┘ └────┬─────┘ └───┬────┘ └───┬────┘
│ │ │ │ │ │
│ Create │ │ │ │ │
│ policy v2 │ │ │ │ │
│────────────>│ │ │ │ │
│ │ │ │ │ │
│ │ Store as │ │ │ │
│ │ draft │ │ │ │
│ │───┐ │ │ │ │
│ │ │ │ │ │ │
│ │<──┘ │ │ │ │
│ │ │ │ │ │
│ Define │ │ │ │ │
│ rollout │ │ │ │ │
│────────────────────────────> │ │ │
│ │ │ │ │ │
│ │ │ Get tenant │ │ │
│ │ │ hierarchy │ │ │
│ │ │────────────>│ │ │
│ │ │ │ │ │
│ │ │ Tenant tree │ │ │
│ │ │<────────────│ │ │
│ │ │ │ │ │
│ Rollout │ │ │ │ │
│ plan │ │ │ │ │
│<──────────────────────────── │ │ │
│ │ │ │ │ │
│ Approve │ │ │ │ │
│────────────────────────────> │ │ │
│ │ │ │ │ │
│ │ │ Stage 1: │ │ │
│ │ │ Canary │ │ │
│ │ │─────────────────────────>│ │
│ │ │ │ │ │
│ │ │ │ │ Apply to │
│ │ │ │ │ canary │
│ │ │ │ │ tenant │
│ │ │ │ │───┐ │
│ │ │ │ │ │ │
│ │ │ │ │<──┘ │
│ │ │ │ │ │
│ │ │ Monitor │ │ │
│ │ │ (24h) │ │ │
│ │ │───┐ │ │ │
│ │ │ │ │ │ │
│ │ │<──┘ │ │ │
│ │ │ │ │ │
│ │ │ Stage 2: │ │ │
│ │ │ 25% tenants │ │ │
│ │ │─────────────────────────>│ │
│ │ │ │ │ │
│ │ │ ... │ │ │
│ │ │ │ │ │
│ │ │ Stage N: │ │ │
│ │ │ 100% │ │ │
│ │ │─────────────────────────>│ │
│ │ │ │ │ │
│ │ │ Complete │ │ │
│ │ │───────────────────────────────────────>
│ │ │ │ │ │
│ Rollout │ │ │ │ │ Notify
│ complete │ │ │ │ │ admins
│<────────────────────────────────────────────────────────────────────
│ │ │ │ │ │
Authorization Scopes
[IMPLEMENTED] — canonical scope claim values from StellaOps.Auth.Abstractions/StellaOpsScopes.cs. These are the scope claim values (some use :, some use .); ASP.NET policy names may differ from the prose. Verify against the scope constant when wiring callers.
| Operation | Scope claim value | Constant |
|---|---|---|
| Create pack / revision / bundle | policy:edit | PolicyEdit |
| List packs / evaluate revision | policy:read | PolicyRead |
| Activate a revision | policy:activate | PolicyActivate |
| Author Policy Studio workspaces | policy:author | PolicyAuthor |
| Submit a draft for review | policy:submit | PolicySubmit |
| Review drafts | policy:review | PolicyReview |
| Approve / reject a policy | policy:approve | PolicyApprove |
| Publish approved versions | policy:publish | PolicyPublish |
| Promote attestations across environments | policy:promote | PolicyPromote |
| Trigger runs / activation workflows | policy:run | PolicyRun |
| Run simulations | policy:simulate | PolicySimulate |
| Audit Policy Studio activity | policy:audit | PolicyAudit |
| Read / write the tenant catalog | authority:tenants.read / authority:tenants.write | AuthorityTenantsRead / AuthorityTenantsWrite |
There is no dedicated
policy:rolloutscope, because no rollout orchestration endpoint exists. A hypothetical rollout engine would most likely composepolicy:promote+policy:activate+authority:tenants.read.
Implemented Lifecycle [IMPLEMENTED]
The flow that real services execute today:
- Author —
POST /api/policy/packscreates a pack container (policy:edit), thenPOST /api/policy/packs/{packId}/revisionsupserts a revision withInitialStatusofDraftorApprovedand an optionalRequiresTwoPersonApprovalflag. - Compile + sign —
POST /api/policy/packs/{packId}/revisions/{v}/bundlecompiles the DSL into an immutable, signed bundle (fails fast on syntax errors / rule conflicts; returns the bundle digest). - Evaluate (dry run) —
POST /api/policy/packs/{packId}/revisions/{v}/evaluateruns the compiled bundle against an advisory/VEX/SBOM context deterministically (policy:read). - Activate —
POST /api/policy/packs/{packId}/revisions/{v}:activate(policy:activate) promotes an Approved revision to Active. When two-person approval is required it returns202 Acceptedwithpending_second_approvaluntil a distinct second approver signs off, then200 OKwithactivated. Activation is audited (AuditActions.Policy.ActivateRevision). - (Library) Environment promotion — via the
StellaOps.Policy.Registrylibrary: aPromotionBindingmoves a pack version into an environment (Dev/Stage/Prod) under modeManual,AutomaticOnApproval,Scheduled, orCanary.PromotionBindingRulesgate this withRequiredApprovers,MinimumApprovals,RequireSuccessfulSimulation,MinimumSimulationInputs,MinimumSoakPeriod, andAllowedSourceEnvironments.RollbackAsyncrestores a prior binding by id or by steps-back (RollbackRequest.TargetBindingId/StepsBack). The only shippedIPolicyPackStoreis the test-harnessInMemoryPolicyPackStore— no durable store implementation exists, and none of these Registry services (IPromotionService,IReviewWorkflowService,IPublishPipelineService) is referenced by theStellaOps.Policy.Enginehost, so the promotion/review/publish surface is not exposed as a Policy HTTP route.
The per-pack review workflow (
IReviewWorkflowService) and publish pipeline (IPublishPipelineService) likewise live in the Registry library (Pending -> InReview -> ChangesRequested/Approved/Rejected/Cancelled; publish statesPublished/Revoked/Superseded). They are not wired to a running Policy service host in this repo.
Step-by-Step (Target Design) [DRAFT / NOT IMPLEMENTED]
Everything in this section describes a rollout engine that does not exist. The payloads are illustrative drafts. The closest implemented analogues are listed in Implemented Lifecycle above.
1. Policy Creation
A policy revision is authored in the DSL (the syntax "stella-dsl@1" version string is real and validated in PolicyParser.cs). The richer rollout-plan metadata fields (version_tag, changes_from_v1) are not part of any compiled contract:
# Policy Set: production-v2
version: "stella-dsl@1"
name: production
version_tag: "v2.0.0"
description: "Updated production policy with KEV blocking"
changes_from_v1:
- added: block-kev-vulnerabilities
- modified: critical-threshold (9.0 → 8.5)
- removed: legacy-exception-rule
rules:
- name: block-kev-vulnerabilities
description: Block any KEV-listed vulnerability
condition: kev == true
action: FAIL
severity: critical
- name: no-critical-reachable
condition: |
severity == 'critical' AND
cvss >= 8.5 AND
reachability IN ['SR', 'RO', 'CR']
action: FAIL
2. Rollout Plan Definition
Platform Admin defines rollout strategy:
{
"rollout_id": "rollout-789",
"policy_set": "production",
"from_version": "v1.0.0",
"to_version": "v2.0.0",
"strategy": "staged",
"stages": [
{
"name": "canary",
"description": "Single low-risk tenant",
"tenants": ["platform-eng-core-services"],
"duration": "24h",
"success_criteria": {
"max_new_failures": 5,
"max_failure_rate_increase": 0.05
},
"auto_proceed": false
},
{
"name": "early-adopters",
"description": "25% of tenants (by scan volume)",
"selection": {
"method": "percentage",
"value": 25,
"weight_by": "scan_volume"
},
"duration": "48h",
"success_criteria": {
"max_new_failures": 20,
"max_failure_rate_increase": 0.10
},
"auto_proceed": true
},
{
"name": "majority",
"description": "75% of tenants",
"selection": {
"method": "percentage",
"value": 75
},
"duration": "24h",
"auto_proceed": true
},
{
"name": "full",
"description": "100% of tenants",
"selection": {
"method": "all"
}
}
],
"rollback": {
"automatic": true,
"triggers": [
{"metric": "failure_rate_increase", "threshold": 0.20},
{"metric": "new_critical_blocks", "threshold": 50}
]
}
}
3. Impact Analysis
Before approval, system analyzes potential impact:
{
"impact_analysis": {
"rollout_id": "rollout-789",
"analysis_date": "2024-12-29T10:00:00Z",
"historical_data_range": "30d",
"results": {
"total_scans_analyzed": 15234,
"predicted_new_failures": 127,
"predicted_failure_rate_change": "+0.83%",
"affected_images": 89,
"by_team": [
{"team": "Product Development", "new_failures": 78},
{"team": "Platform Engineering", "new_failures": 31},
{"team": "Security", "new_failures": 18}
],
"top_triggered_rules": [
{"rule": "block-kev-vulnerabilities", "count": 45},
{"rule": "no-critical-reachable", "count": 82}
],
"recommendation": "PROCEED_WITH_CAUTION"
}
}
}
4. Approval and Initiation
Policy Admin approves rollout after review:
{
"approval": {
"rollout_id": "rollout-789",
"approved_by": "policy-admin@acme.com",
"approved_at": "2024-12-29T11:00:00Z",
"approval_notes": "Impact acceptable. Notified affected teams.",
"notifications_sent": [
{"channel": "slack", "target": "#platform-engineering"},
{"channel": "email", "target": "team-leads@acme.com"}
]
}
}
5. Staged Execution
Scheduler executes each stage:
Stage 1: Canary
{
"stage_execution": {
"rollout_id": "rollout-789",
"stage": "canary",
"started_at": "2024-12-29T11:00:00Z",
"tenants_activated": ["platform-eng-core-services"],
"status": "monitoring"
}
}
Stage Monitoring
{
"stage_metrics": {
"rollout_id": "rollout-789",
"stage": "canary",
"monitored_period": "24h",
"metrics": {
"scans_evaluated": 234,
"new_failures": 3,
"failure_rate_before": 0.12,
"failure_rate_after": 0.13,
"success_criteria_met": true
}
}
}
6. Progressive Rollout
After canary success, proceed to next stages:
{
"stage_progression": {
"rollout_id": "rollout-789",
"completed_stages": ["canary", "early-adopters", "majority"],
"current_stage": "full",
"tenants_on_v2": 47,
"tenants_on_v1": 0,
"total_rollout_duration": "96h",
"status": "completed"
}
}
7. Rollback (If Needed)
If success criteria not met, automatic rollback:
{
"rollback": {
"rollout_id": "rollout-789",
"triggered_at": "2024-12-30T15:30:00Z",
"trigger_reason": "failure_rate_increase exceeded 0.20 threshold",
"rollback_stage": "early-adopters",
"tenants_rolled_back": 12,
"action": "reverted to v1.0.0",
"notifications_sent": true
}
}
Rollout Strategies [DRAFT / NOT IMPLEMENTED]
No strategy executor exists. The only strategy enum in source is
PromotionBindingMode(Manual,AutomaticOnApproval,Scheduled,Canary) on a single per-environment binding — there is no blue-green, canary-traffic-split, or feature-flag engine. The YAML blocks below are design sketches only.
Blue-Green
strategy: blue_green
config:
parallel_evaluation: true # Both versions evaluated
comparison_period: 24h
switch_threshold:
verdict_match_rate: 0.95
Canary with Traffic Split
strategy: canary_traffic
config:
initial_percentage: 5
increment: 10
increment_interval: 4h
max_error_rate: 0.01
Feature Flag
strategy: feature_flag
config:
flag_name: "policy-v2-enabled"
default: false
overrides:
- tenant: "security-team"
value: true
Data Contracts [DRAFT / NOT IMPLEMENTED]
These TypeScript interfaces do not correspond to any emitted contract. The real, implemented contracts are the C# records in
StellaOps.Policy.Registry/Services/(PromotionBinding,PromoteRequest,RollbackRequest,PromotionBindingRules,PublishResult,ReviewRequest, etc.) and the Policy EnginePolicyPackDto/PolicyRevisionDto/PolicyRevisionActivationResponserecords inEndpoints/PolicyPackEndpoints.cs. See Implemented Lifecycle.
Rollout Plan Schema (draft)
interface RolloutPlan {
rollout_id: string;
policy_set: string;
from_version: string;
to_version: string;
strategy: 'staged' | 'blue_green' | 'canary_traffic' | 'feature_flag';
stages: Stage[];
rollback: {
automatic: boolean;
triggers: RollbackTrigger[];
};
notifications: NotificationConfig[];
}
interface Stage {
name: string;
description?: string;
tenants?: string[];
selection?: TenantSelection;
duration?: string; // ISO-8601 duration
success_criteria?: SuccessCriteria;
auto_proceed?: boolean;
}
Rollout Status Schema (draft)
interface RolloutStatus {
rollout_id: string;
status: 'pending' | 'in_progress' | 'paused' | 'completed' | 'rolled_back' | 'failed';
current_stage?: string;
stages: Array<{
name: string;
status: 'pending' | 'active' | 'monitoring' | 'completed' | 'failed';
started_at?: string;
completed_at?: string;
metrics?: StageMetrics;
}>;
tenant_status: Array<{
tenant_id: string;
policy_version: string;
activated_at?: string;
}>;
}
Policy Inheritance [DRAFT / NOT IMPLEMENTED]
No policy inheritance /
inherits_fromresolution exists insrc/. There is no org/team/project tenant tree and no cascading override resolver. The Risk Profile model does have a single-levelExtendsfield (profile.ExtendsinRiskProfileModel) that one profile can use to extend another profile by id — but that is per-profile inheritance, not the tenant-hierarchy cascade depicted here. Treat the diagram below as a roadmap sketch.
Organization Policy (base)
└── inherits_from: stellaops-default
Team Policy (override)
└── inherits_from: organization
└── overrides: [severity-thresholds]
Project Policy (final)
└── inherits_from: team
└── overrides: [specific-exceptions]
Resolution order (target): Project → Team → Organization → Platform Default
Error Handling [DRAFT / NOT IMPLEMENTED]
Stage-level recovery requires the rollout engine, which does not exist. The implemented error paths are per-operation: revision activation rejects non-Approved revisions (
400 NotApproved), duplicate approvals (400 DuplicateApproval), and missing pack/revision (404); the RegistryRollbackAsyncreturnsRollbackResult.Success = falsewith anErrorwhen a prior binding cannot be restored.
| Error (proposed) | Recovery |
|---|---|
| Stage timeout | Pause rollout, alert admin |
| Metrics unavailable | Use last known good, extend monitoring |
| Tenant unreachable | Skip tenant, continue with others |
| Rollback failure | Manual intervention required |
Observability
Metrics
[DRAFT / NOT IMPLEMENTED] — the policy_rollout_* and policy_version_active metrics below are not emitted by any service.
| Metric (proposed) | Type | Labels |
|---|---|---|
policy_rollout_status | Gauge | rollout_id, stage |
policy_rollout_tenant_count | Gauge | rollout_id, version |
policy_rollout_failures_total | Counter | rollout_id, stage |
policy_version_active | Gauge | tenant, policy_set, version |
[IMPLEMENTED] — the metrics actually emitted today come from the StellaOps.Policy.Registry library (meter StellaOps.Policy.Registry, see Telemetry/PolicyRegistryMetrics.cs):
| Metric | Instrument |
|---|---|
policy_registry.packs.created | Counter |
policy_registry.packs.published | Counter |
policy_registry.packs.revoked | Counter |
policy_registry.promotions.total | Counter |
policy_registry.rollbacks.total | Counter |
policy_registry.reviews.submitted / .approved / .rejected | Counter |
policy_registry.compilations.total / .errors | Counter |
policy_registry.simulations.total / policy_registry.batch_simulations.total | Counter |
policy_registry.violations.total | Counter |
policy_registry.compilation.duration / .simulation.duration / .batch_simulation.duration | Histogram |
policy_registry.pack.rules / .simulation.violations / .batch_simulation.inputs | Histogram |
policy_registry.packs.active / .reviews.pending / .batch_jobs.running | Observable gauge |
Key Log Events
[DRAFT / NOT IMPLEMENTED] — the rollout.* events below are not emitted; no rollout engine exists.
| Event (proposed) | Level | Fields |
|---|---|---|
rollout.created | INFO | rollout_id, policy_set, stages |
rollout.stage.started | INFO | rollout_id, stage, tenants |
rollout.stage.completed | INFO | rollout_id, stage, metrics |
rollout.rollback | WARN | rollout_id, reason, tenants |
rollout.completed | INFO | rollout_id, duration |
[IMPLEMENTED] — the notification/audit events that exist today:
| Event | Source |
|---|---|
policy.profile.created | PolicyProfileNotificationEventTypes |
policy.profile.activated / policy.profile.deactivated | PolicyProfileNotificationEventTypes |
policy.profile.threshold_changed | PolicyProfileNotificationEventTypes |
policy.profile.override_added / policy.profile.override_removed | PolicyProfileNotificationEventTypes |
policy.profile.simulation_ready | PolicyProfileNotificationEventTypes |
AuditActions.Policy.ActivateRevision (revision activation) | Policy Engine audit emission |
Related Flows
- Policy Evaluation Flow - Policy application
- Exception Approval Workflow - Exception handling
- Notification Flow - Rollout alerts
