19 KiB
Facet Sealing Architecture
Ownership: Scanner Guild, Policy Guild Audience: Service owners, platform engineers, security architects Related: Platform Architecture, Scanner Architecture, Replay Architecture, Policy Engine
This dossier describes the Facet Sealing subsystem, which provides cryptographically sealed manifests for logical slices of container images, enabling fine-grained drift detection, per-facet quota enforcement, and deterministic change tracking.
1. Overview
A Facet is a declared logical slice of a container image representing a cohesive set of files with shared characteristics:
| Facet Type | Description | Examples |
|---|---|---|
os |
Operating system packages | /var/lib/dpkg/**, /var/lib/rpm/** |
lang/<ecosystem> |
Language-specific dependencies | node_modules/**, site-packages/**, vendor/** |
binary |
Native binaries and shared libraries | /usr/bin/*, /lib/**/*.so* |
config |
Configuration files | /etc/**, *.conf, *.yaml |
custom |
User-defined patterns | Project-specific paths |
Each facet can be individually sealed (cryptographic snapshot) and monitored for drift (changes between seals).
2. System Landscape
graph TD
subgraph Scanner["Scanner Services"]
FE[FacetExtractor]
FH[FacetHasher]
MB[MerkleBuilder]
end
subgraph Storage["Facet Storage"]
FS[(PostgreSQL<br/>facet_seals)]
FC[(CAS<br/>facet_manifests)]
end
subgraph Policy["Policy & Enforcement"]
DC[DriftCalculator]
QE[QuotaEnforcer]
AV[AdmissionValidator]
end
subgraph Signing["Attestation"]
DS[DSSE Signer]
AT[Attestor]
end
subgraph CLI["CLI & Integration"]
SealCmd[stella seal]
DriftCmd[stella drift]
VexCmd[stella vex gen]
Zastava[Zastava Webhook]
end
FE --> FH
FH --> MB
MB --> DS
DS --> FS
DS --> FC
FS --> DC
DC --> QE
QE --> AV
AV --> Zastava
SealCmd --> FE
DriftCmd --> DC
VexCmd --> DC
3. Core Data Models
3.1 FacetDefinition
Declares a facet with its extraction patterns and quota constraints:
public sealed record FacetDefinition
{
public required string FacetId { get; init; } // e.g., "os", "lang/node", "binary"
public required FacetType Type { get; init; } // OS, LangNode, LangPython, Binary, Config, Custom
public required ImmutableArray<string> IncludeGlobs { get; init; }
public ImmutableArray<string> ExcludeGlobs { get; init; } = [];
public FacetQuota? Quota { get; init; }
}
public enum FacetType
{
OS,
LangNode,
LangPython,
LangGo,
LangRust,
LangJava,
LangDotNet,
Binary,
Config,
Custom
}
3.2 FacetManifest
Per-facet file manifest with Merkle root:
public sealed record FacetManifest
{
public required string FacetId { get; init; }
public required FacetType Type { get; init; }
public required ImmutableArray<FacetFileEntry> Files { get; init; }
public required string MerkleRoot { get; init; } // SHA-256 hex
public required int FileCount { get; init; }
public required long TotalBytes { get; init; }
public required DateTimeOffset ExtractedAt { get; init; }
public required string ExtractorVersion { get; init; }
}
public sealed record FacetFileEntry
{
public required string Path { get; init; } // Normalized POSIX path
public required string ContentHash { get; init; } // SHA-256 hex
public required long Size { get; init; }
public required string Mode { get; init; } // POSIX mode string "0644"
public required DateTimeOffset ModTime { get; init; } // Normalized to UTC
}
3.3 FacetSeal
DSSE-signed seal combining manifest with metadata:
public sealed record FacetSeal
{
public required Guid SealId { get; init; }
public required string ImageRef { get; init; } // registry/repo:tag@sha256:...
public required string ImageDigest { get; init; } // sha256:...
public required FacetManifest Manifest { get; init; }
public required DateTimeOffset SealedAt { get; init; }
public required string SealedBy { get; init; } // Identity/service
public required FacetQuota? AppliedQuota { get; init; }
public required DsseEnvelope Envelope { get; init; }
}
3.4 FacetQuota
Per-facet change budget:
public sealed record FacetQuota
{
public required string FacetId { get; init; }
public double MaxChurnPercent { get; init; } = 5.0; // 0-100
public int MaxChangedFiles { get; init; } = 50;
public int MaxAddedFiles { get; init; } = 25;
public int MaxRemovedFiles { get; init; } = 10;
public QuotaAction OnExceed { get; init; } = QuotaAction.Warn;
}
public enum QuotaAction
{
Warn, // Log warning, allow admission
Block, // Reject admission
RequireVex // Require VEX justification before admission
}
3.5 FacetDrift
Drift calculation result between two seals:
public sealed record FacetDrift
{
public required string FacetId { get; init; }
public required Guid BaselineSealId { get; init; }
public required Guid CurrentSealId { get; init; }
public required ImmutableArray<DriftEntry> Added { get; init; }
public required ImmutableArray<DriftEntry> Removed { get; init; }
public required ImmutableArray<DriftEntry> Modified { get; init; }
public required DriftScore Score { get; init; }
public required QuotaVerdict QuotaVerdict { get; init; }
}
public sealed record DriftEntry
{
public required string Path { get; init; }
public string? OldHash { get; init; }
public string? NewHash { get; init; }
public long? OldSize { get; init; }
public long? NewSize { get; init; }
public DriftCause Cause { get; init; } = DriftCause.Unknown;
}
public enum DriftCause
{
Unknown,
PackageUpdate,
ConfigChange,
BinaryRebuild,
NewDependency,
RemovedDependency,
SecurityPatch
}
public sealed record DriftScore
{
public required int TotalChanges { get; init; }
public required double ChurnPercent { get; init; }
public required int AddedCount { get; init; }
public required int RemovedCount { get; init; }
public required int ModifiedCount { get; init; }
}
public sealed record QuotaVerdict
{
public required bool Passed { get; init; }
public required ImmutableArray<QuotaViolation> Violations { get; init; }
public required QuotaAction RecommendedAction { get; init; }
}
public sealed record QuotaViolation
{
public required string QuotaField { get; init; } // e.g., "MaxChurnPercent"
public required double Limit { get; init; }
public required double Actual { get; init; }
public required string Message { get; init; }
}
4. Component Architecture
4.1 FacetExtractor
Extracts file entries from container images based on facet definitions:
public interface IFacetExtractor
{
Task<FacetManifest> ExtractAsync(
string imageRef,
FacetDefinition definition,
CancellationToken ct = default);
Task<ImmutableArray<FacetManifest>> ExtractAllAsync(
string imageRef,
ImmutableArray<FacetDefinition> definitions,
CancellationToken ct = default);
}
Implementation notes:
- Uses existing
ISurfaceReaderfor container layer traversal - Normalizes paths to POSIX format (forward slashes, no trailing slashes)
- Computes SHA-256 content hashes for each file
- Normalizes timestamps to UTC, mode to POSIX string
- Sorts files lexicographically for deterministic ordering
4.2 FacetHasher
Computes Merkle tree for facet file entries:
public interface IFacetHasher
{
FacetMerkleResult ComputeMerkle(ImmutableArray<FacetFileEntry> files);
}
public sealed record FacetMerkleResult
{
public required string Root { get; init; }
public required ImmutableArray<string> LeafHashes { get; init; }
public required ImmutableArray<MerkleProofNode> Proof { get; init; }
}
Implementation notes:
- Leaf hash = SHA-256(path || contentHash || size || mode)
- Binary Merkle tree with lexicographic leaf ordering
- Empty facet produces well-known empty root hash
- Proof enables verification of individual file membership
4.3 FacetSealStore
PostgreSQL storage for sealed facet manifests:
-- Core seal storage
CREATE TABLE facet_seals (
seal_id UUID PRIMARY KEY,
tenant TEXT NOT NULL,
image_ref TEXT NOT NULL,
image_digest TEXT NOT NULL,
facet_id TEXT NOT NULL,
facet_type TEXT NOT NULL,
merkle_root TEXT NOT NULL,
file_count INTEGER NOT NULL,
total_bytes BIGINT NOT NULL,
sealed_at TIMESTAMPTZ NOT NULL,
sealed_by TEXT NOT NULL,
quota_json JSONB,
manifest_cas TEXT NOT NULL, -- CAS URI to full manifest
dsse_envelope JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_facet_seal UNIQUE (tenant, image_digest, facet_id)
);
CREATE INDEX ix_facet_seals_image ON facet_seals (tenant, image_digest);
CREATE INDEX ix_facet_seals_merkle ON facet_seals (merkle_root);
-- Drift history
CREATE TABLE facet_drift_history (
drift_id UUID PRIMARY KEY,
tenant TEXT NOT NULL,
baseline_seal_id UUID NOT NULL REFERENCES facet_seals(seal_id),
current_seal_id UUID NOT NULL REFERENCES facet_seals(seal_id),
facet_id TEXT NOT NULL,
drift_score_json JSONB NOT NULL,
quota_verdict_json JSONB NOT NULL,
computed_at TIMESTAMPTZ NOT NULL,
CONSTRAINT uq_drift_pair UNIQUE (baseline_seal_id, current_seal_id)
);
4.4 DriftCalculator
Computes drift between baseline and current seals:
public interface IDriftCalculator
{
Task<FacetDrift> CalculateAsync(
Guid baselineSealId,
Guid currentSealId,
CancellationToken ct = default);
Task<ImmutableArray<FacetDrift>> CalculateAllAsync(
string imageDigestBaseline,
string imageDigestCurrent,
CancellationToken ct = default);
}
Implementation notes:
- Retrieves manifests from CAS via seal metadata
- Performs set difference operations on file paths
- Detects modifications via content hash comparison
- Attributes drift causes where determinable (e.g., package manager metadata)
4.5 QuotaEnforcer
Evaluates drift against quota constraints:
public interface IQuotaEnforcer
{
QuotaVerdict Evaluate(FacetDrift drift, FacetQuota quota);
Task<ImmutableArray<QuotaVerdict>> EvaluateAllAsync(
ImmutableArray<FacetDrift> drifts,
ImmutableDictionary<string, FacetQuota> quotas,
CancellationToken ct = default);
}
4.6 AdmissionValidator
Zastava webhook integration for admission control:
public interface IFacetAdmissionValidator
{
Task<AdmissionResult> ValidateAsync(
AdmissionRequest request,
CancellationToken ct = default);
}
public sealed record AdmissionResult
{
public required bool Allowed { get; init; }
public string? Message { get; init; }
public ImmutableArray<QuotaViolation> Violations { get; init; } = [];
public string? RequiredVexStatement { get; init; }
}
5. DSSE Envelope Structure
Facet seals use DSSE (Dead Simple Signing Envelope) for cryptographic binding:
{
"payloadType": "application/vnd.stellaops.facet-seal.v1+json",
"payload": "<base64url-encoded canonical JSON of FacetSeal>",
"signatures": [
{
"keyid": "sha256:abc123...",
"sig": "<base64url-encoded signature>"
}
]
}
Payload structure (canonical JSON, RFC 8785):
{
"_type": "https://stellaops.io/FacetSeal/v1",
"facetId": "os",
"facetType": "OS",
"imageDigest": "sha256:abc123...",
"imageRef": "registry.example.com/app:v1.2.3",
"manifest": {
"extractedAt": "2026-01-05T10:00:00.000Z",
"extractorVersion": "1.0.0",
"fileCount": 1234,
"files": [
{
"contentHash": "sha256:...",
"mode": "0644",
"modTime": "2026-01-01T00:00:00.000Z",
"path": "/etc/os-release",
"size": 256
}
],
"merkleRoot": "sha256:def456...",
"totalBytes": 1048576
},
"quota": {
"maxAddedFiles": 25,
"maxChangedFiles": 50,
"maxChurnPercent": 5.0,
"maxRemovedFiles": 10,
"onExceed": "Warn"
},
"sealId": "550e8400-e29b-41d4-a716-446655440000",
"sealedAt": "2026-01-05T10:05:00.000Z",
"sealedBy": "scanner-worker-01"
}
6. Default Facet Definitions
Standard facet definitions applied when no custom configuration is provided:
# Default facet configuration
facets:
- facetId: os
type: OS
includeGlobs:
- /var/lib/dpkg/**
- /var/lib/rpm/**
- /var/lib/pacman/**
- /var/lib/apk/**
- /var/cache/apt/**
- /etc/apt/**
- /etc/yum.repos.d/**
excludeGlobs:
- "**/*.log"
quota:
maxChurnPercent: 5.0
maxChangedFiles: 100
onExceed: Warn
- facetId: lang/node
type: LangNode
includeGlobs:
- "**/node_modules/**"
- "**/package.json"
- "**/package-lock.json"
- "**/yarn.lock"
- "**/pnpm-lock.yaml"
quota:
maxChurnPercent: 10.0
maxChangedFiles: 500
onExceed: RequireVex
- facetId: lang/python
type: LangPython
includeGlobs:
- "**/site-packages/**"
- "**/dist-packages/**"
- "**/requirements.txt"
- "**/Pipfile.lock"
- "**/poetry.lock"
quota:
maxChurnPercent: 10.0
maxChangedFiles: 200
onExceed: Warn
- facetId: lang/go
type: LangGo
includeGlobs:
- "**/go.mod"
- "**/go.sum"
- "**/vendor/**"
quota:
maxChurnPercent: 15.0
maxChangedFiles: 100
onExceed: Warn
- facetId: binary
type: Binary
includeGlobs:
- /usr/bin/*
- /usr/sbin/*
- /bin/*
- /sbin/*
- /usr/lib/**/*.so*
- /lib/**/*.so*
- /usr/local/bin/*
excludeGlobs:
- "**/*.py"
- "**/*.sh"
quota:
maxChurnPercent: 2.0
maxChangedFiles: 20
onExceed: Block
- facetId: config
type: Config
includeGlobs:
- /etc/**
- "**/*.conf"
- "**/*.cfg"
- "**/*.ini"
- "**/*.yaml"
- "**/*.yml"
- "**/*.json"
excludeGlobs:
- /etc/passwd
- /etc/shadow
- /etc/group
- "**/*.log"
quota:
maxChurnPercent: 20.0
maxChangedFiles: 50
onExceed: Warn
7. Integration Points
7.1 Scanner Integration
Scanner invokes facet extraction during scan:
// In ScanOrchestrator
var facetDefs = await _facetConfigLoader.LoadAsync(scanRequest.FacetConfig, ct);
var manifests = await _facetExtractor.ExtractAllAsync(imageRef, facetDefs, ct);
foreach (var manifest in manifests)
{
var seal = await _facetSealer.SealAsync(manifest, scanRequest, ct);
await _facetSealStore.SaveAsync(seal, ct);
}
7.2 CLI Integration
# Seal all facets for an image
stella seal myregistry.io/app:v1.2.3 --output seals.json
# Seal specific facets
stella seal myregistry.io/app:v1.2.3 --facet os --facet lang/node
# Check drift between two image versions
stella drift myregistry.io/app:v1.2.3 myregistry.io/app:v1.2.4 --format json
# Generate VEX from drift
stella vex gen --from-drift myregistry.io/app:v1.2.3 myregistry.io/app:v1.2.4
7.3 Zastava Webhook Integration
// In FacetAdmissionValidator
public async Task<AdmissionResult> ValidateAsync(AdmissionRequest request, CancellationToken ct)
{
// Find baseline seal (latest approved)
var baseline = await _sealStore.GetLatestApprovedAsync(request.ImageRef, ct);
if (baseline is null)
return AdmissionResult.Allowed("No baseline seal found, skipping facet check");
// Extract current facets
var currentManifests = await _extractor.ExtractAllAsync(request.ImageRef, _defaultFacets, ct);
// Calculate drift for each facet
var drifts = new List<FacetDrift>();
foreach (var manifest in currentManifests)
{
var baselineSeal = baseline.FirstOrDefault(s => s.FacetId == manifest.FacetId);
if (baselineSeal is not null)
{
var drift = await _driftCalculator.CalculateAsync(baselineSeal, manifest, ct);
drifts.Add(drift);
}
}
// Evaluate quotas
var violations = new List<QuotaViolation>();
QuotaAction maxAction = QuotaAction.Warn;
foreach (var drift in drifts)
{
var verdict = _quotaEnforcer.Evaluate(drift, drift.AppliedQuota);
if (!verdict.Passed)
{
violations.AddRange(verdict.Violations);
if (verdict.RecommendedAction > maxAction)
maxAction = verdict.RecommendedAction;
}
}
return maxAction switch
{
QuotaAction.Block => AdmissionResult.Denied(violations),
QuotaAction.RequireVex => AdmissionResult.RequiresVex(violations),
_ => AdmissionResult.Allowed(violations)
};
}
8. Observability
8.1 Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
facet_seal_total |
Counter | tenant, facet_type, status |
Total seals created |
facet_seal_duration_seconds |
Histogram | facet_type |
Time to create seal |
facet_drift_score |
Gauge | tenant, facet_id, image |
Current drift score |
facet_quota_violations_total |
Counter | tenant, facet_id, quota_field |
Quota violations |
facet_admission_decisions_total |
Counter | tenant, decision, facet_id |
Admission decisions |
8.2 Traces
facet.extract - Facet file extraction from image
facet.hash - Merkle tree computation
facet.seal - DSSE signing
facet.drift.compute - Drift calculation
facet.quota.evaluate - Quota enforcement
facet.admission - Admission validation
8.3 Logs
Structured log fields:
facetId: Facet identifierimageRef: Container image referenceimageDigest: Image content digestmerkleRoot: Facet Merkle rootdriftScore: Computed drift percentagequotaVerdict: Pass/fail status
9. Security Considerations
- Signature Verification: All seals must be DSSE-signed with keys managed by Authority service
- Tenant Isolation: Seals are scoped to tenants; cross-tenant access is prohibited
- Immutability: Once created, seals cannot be modified; only superseded by new seals
- Audit Trail: All seal operations are logged with correlation IDs
- Key Rotation: Signing keys support rotation; old signatures remain valid with archived keys
10. References
- DSSE Specification
- RFC 8785 - JSON Canonicalization
- Scanner Architecture
- Attestor Architecture
- Policy Engine Architecture
- Replay Architecture
Last updated: 2026-01-05