Files
git.stella-ops.org/docs/modules/facet/architecture.md

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 ISurfaceReader for 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 identifier
  • imageRef: Container image reference
  • imageDigest: Image content digest
  • merkleRoot: Facet Merkle root
  • driftScore: Computed drift percentage
  • quotaVerdict: Pass/fail status

9. Security Considerations

  1. Signature Verification: All seals must be DSSE-signed with keys managed by Authority service
  2. Tenant Isolation: Seals are scoped to tenants; cross-tenant access is prohibited
  3. Immutability: Once created, seals cannot be modified; only superseded by new seals
  4. Audit Trail: All seal operations are logged with correlation IDs
  5. Key Rotation: Signing keys support rotation; old signatures remain valid with archived keys

10. References


Last updated: 2026-01-05