Tile-Proxy Service Design

Overview

The Tile-Proxy service acts as an intermediary between StellaOps clients and upstream Rekor transparency log APIs. It provides centralized tile caching, request coalescing, and offline support for air-gapped environments.

Offline default (2026-07-12, EVI-3 / sprint SPRINT_20260712_003). The service ships with no upstream (tile_proxy:UpstreamUrl is empty) and the sync job disabled. In that state it serves only the tiles already present in its content-addressed cache and performs zero network egress: a cache miss returns 503 upstream_not_configured (not 502, which means an upstream we do have failed), and GET /_admin/ready reports {"ready": true, "mode": "offline-cache-only"} rather than probing upstream — a readiness probe must not become the thing that dials out.

Choosing an upstream is an explicit operator act. Enabling Sync.Enabled without an upstream is rejected at startup (TileProxyOptionsValidator) — the service never falls back to a hardcoded public log. See “Configuration” below and devops/compose/README.md § Tile Proxy.

Architecture

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  CI/CD Agents   │────►│   Tile Proxy    │────►│   Rekor API     │
│  (StellaOps)    │     │   (StellaOps)   │     │   (Upstream)    │
└─────────────────┘     └────────┬────────┘     └─────────────────┘
                                 │
         ┌───────────────────────┼───────────────────────┐
         │                       │                       │
         ▼                       ▼                       ▼
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  Tile Cache     │     │  TUF Metadata   │     │  Checkpoint     │
│  (CAS Store)    │     │  (TrustRepo)    │     │  Cache          │
└─────────────────┘     └─────────────────┘     └─────────────────┘

Core Responsibilities

  1. Tile Proxying: Forward tile requests to upstream Rekor, caching responses locally
  2. Content-Addressed Storage: Store tiles by hash for deduplication and immutability
  3. TUF Integration: Optionally validate metadata using TUF trust anchors
  4. Request Coalescing: Deduplicate concurrent requests for the same tile
  5. Checkpoint Caching: Cache and serve recent checkpoints
  6. Offline Mode: Serve from cache when upstream is unavailable

API Surface

Proxy Endpoints (Passthrough)

EndpointDescription
GET /tile/{level}/{index}Proxy tile request (cache-through)
GET /tile/{level}/{index}.p/{partialWidth}Proxy partial tile
GET /checkpointProxy checkpoint request
GET /api/v1/log/entries/{uuid}Proxy entry lookup

Admin Endpoints

EndpointDescription
GET /_admin/cache/statsCache statistics (hits, misses, size)
POST /_admin/cache/syncTrigger manual sync job
DELETE /_admin/cache/prunePrune old tiles
GET /_admin/healthHealth check
GET /_admin/readyReadiness check

Caching Strategy

Content-Addressed Tile Storage

Tiles are stored using content-addressed paths based on SHA-256 hash:

{cache_root}/
├── tiles/
│   ├── {origin_hash}/
│   │   ├── {level}/
│   │   │   ├── {index}.tile
│   │   │   └── {index}.meta.json
│   │   └── checkpoints/
│   │       └── {tree_size}.checkpoint
│   └── ...
└── metadata/
    └── cache_stats.json

Tile Metadata

Each tile has associated metadata:

{
  "cachedAt": "2026-01-25T10:00:00Z",
  "treeSize": 1050000,
  "isPartial": false,
  "contentHash": "sha256:abc123...",
  "upstreamUrl": "https://rekor.sigstore.dev"
}

Eviction Policy

  1. LRU by Access Time: Least recently accessed tiles evicted first
  2. Max Size Limit: Configurable maximum cache size
  3. TTL Override: Force re-fetch after configurable time (for checkpoints)
  4. Immutability Preservation: Full tiles (width=256) never evicted unless explicitly pruned

Request Coalescing

Concurrent requests for the same tile are coalesced:

// Pseudo-code for request coalescing
var key = $"{origin}/{level}/{index}";
if (_inflightRequests.TryGetValue(key, out var existing))
{
    return await existing; // Wait for in-flight request
}

var tcs = new TaskCompletionSource<byte[]>();
_inflightRequests[key] = tcs.Task;
try
{
    var tile = await FetchFromUpstream(origin, level, index);
    tcs.SetResult(tile);
    return tile;
}
finally
{
    _inflightRequests.Remove(key);
}

TUF Integration Point

When TufValidationEnabled is true:

  1. Load service map from TUF to discover Rekor URL
  2. Validate Rekor public key from TUF targets
  3. Verify checkpoint signatures using TUF-loaded keys
  4. Reject tiles if checkpoint signature invalid

Upstream Failover

Support multiple upstream sources with failover:

tile_proxy:
  upstreams:
    - url: https://rekor.sigstore.dev
      priority: 1
      timeout: 30s
    - url: https://rekor-mirror.internal
      priority: 2
      timeout: 10s

Failover behavior:

  1. Try primary upstream first
  2. On timeout/error, try next upstream
  3. Cache successful source for subsequent requests
  4. Reset failover state on explicit refresh

Deployment Model

Standalone Service

Run as dedicated service with persistent volume:

Shipped as devops/compose/docker-compose.tile-proxy.yml — an opt-in overlay: it requires TILE_PROXY_UPSTREAM_URL and compose refuses to render without it.

services:
  tile-proxy:
    image: stellaops/tile-proxy:latest
    ports:
      - "8090:8080"
    volumes:
      - tile-cache:/var/cache/stellaops/tiles
      - tuf-cache:/var/cache/stellaops/tuf
    environment:
      # No default — the operator must name the log they trust.
      - TILE_PROXY__tile_proxy__UpstreamUrl=${TILE_PROXY_UPSTREAM_URL:?required}
      - TILE_PROXY__tile_proxy__Origin=${TILE_PROXY_ORIGIN:-stellaops-tileproxy}
      - TILE_PROXY__tile_proxy__Sync__Enabled=${TILE_PROXY_SYNC_ENABLED:-false}
      - TILE_PROXY__tile_proxy__Tuf__Url=${TILE_PROXY_TUF_ROOT_URL:-}

The base stack’s attestor-tileproxy service takes the same settings via ATTESTOR_TILEPROXY_UPSTREAM_URL / ATTESTOR_TILEPROXY_ORIGIN / ATTESTOR_TILEPROXY_SYNC_ENABLED, all empty/false by default.

Sidecar Mode

Run alongside attestor service:

services:
  attestor:
    image: stellaops/attestor:latest
    environment:
      - ATTESTOR__REKOR_URL=http://localhost:8090  # Use sidecar

  tile-proxy:
    image: stellaops/tile-proxy:latest
    network_mode: "service:attestor"

Metrics

Prometheus metrics exposed at /_admin/metrics:

MetricTypeDescription
tile_proxy_cache_hits_totalCounterTotal cache hits
tile_proxy_cache_misses_totalCounterTotal cache misses
tile_proxy_cache_size_bytesGaugeCurrent cache size
tile_proxy_upstream_requests_totalCounterUpstream requests by status
tile_proxy_request_duration_secondsHistogramRequest latency
tile_proxy_sync_last_success_timestampGaugeLast successful sync time

Configuration

Section tile_proxy; env prefix TILE_PROXY__ (so the env form of tile_proxy:UpstreamUrl is TILE_PROXY__tile_proxy__UpstreamUrl). Keys are PascalCase: the configuration binder matches property names case-insensitively but does not strip underscores, so a snake_case key (upstream_url, max_size_gb, …) binds to nothing and silently leaves the C# default in force.

Shipped defaults (src/Attestor/StellaOps.Attestor.TileProxy/appsettings.json) — offline:

{
  "tile_proxy": {
    "UpstreamUrl": "",                 // no upstream: cache-only, zero egress
    "Origin": "stellaops-tileproxy",   // cache namespace
    "Cache":   { "MaxSizeGb": 10, "EvictionPolicy": "lru", "CheckpointTtlMinutes": 5 },
    "Tuf":     { "Enabled": false, "ValidateCheckpointSignature": true },
    "Sync":    { "Enabled": false, "Schedule": "0 */6 * * *", "Depth": 10000 },
    "Request": { "CoalescingEnabled": true, "CoalescingMaxWaitMs": 5000, "TimeoutSeconds": 30 },
    "Failover":{ "Enabled": false, "RetryCount": 2, "RetryDelayMs": 1000 }
  }
}

Operator opt-in (connected deployment; values are examples, not defaults):

{
  "tile_proxy": {
    "UpstreamUrl": "https://rekor.sigstore.dev",          // or your own Rekor/Tessera
    "Origin": "rekor.sigstore.dev - 1985497715",          // that log's checkpoint origin
    "TileBaseUrl": null,                                  // defaults to {UpstreamUrl}/tile/
    "Tuf":  { "Enabled": true, "Url": "https://trust.internal.example/tuf/" },
    "Sync": { "Enabled": true, "Schedule": "0 */6 * * *", "Depth": 10000 }
  }
}

Startup validation: UpstreamUrl may be empty (offline default) but must be an absolute URI when set; Sync.Enabled requires an UpstreamUrl; Origin is required; Tuf.Url is required when Tuf.Enabled.

Security Considerations

Verified against src/ HEAD, 2026-07-12 (CLO-1). TileProxy wires no authentication or authorization middleware at all (no AddAuthentication/UseAuthorization, no client-cert validation, no rate limiter in Program.cs). The entire /_admin group is anonymous — and it is not read-only: POST /_admin/cache/sync triggers the sync job and DELETE /_admin/cache/prune deletes cached tiles. Items 2–4 below are aspirational, not implemented. This is acceptable only on a trusted internal network fronted by the router; a direct-exposed deployment must add a network ACL or reverse-proxy auth in front of /_admin.

  1. No Authentication (current behaviour, not just a default): intended for internal-network use behind the router; the /_admin group (incl. state-mutating sync/prune) is unauthenticated.
  2. Optional mTLS (NOT IMPLEMENTED — future): client-certificate validation is not wired in the TileProxy host.
  3. Rate Limiting (NOT IMPLEMENTED — future): no per-client-IP limiter is registered.
  4. Audit Logging (NOT IMPLEMENTED — future): cache operations are not emitted to an audit sink.
  5. Immutable Tiles: Full tiles are never modified after caching.

Error Handling

ScenarioBehavior
No upstream configured (shipped default)Serve from cache; on a miss return 503 upstream_not_configured. Never dials out.
Upstream unavailableServe from cache if available (marked stale); 502 otherwise
Invalid tile dataReject, don’t cache, log error
Cache fullEvict LRU tiles, continue serving
TUF validation failsReject request, return 502
Checkpoint staleRefresh from upstream, warn in logs

Future Enhancements

  1. Tile Prefetching: Prefetch tiles for known verification patterns
  2. Multi-Log Support: Support multiple transparency logs
  3. Replication: Sync cache between proxy instances
  4. Compression: Optional tile compression for storage