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:UpstreamUrlis 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 returns503 upstream_not_configured(not502, which means an upstream we do have failed), andGET /_admin/readyreports{"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.Enabledwithout an upstream is rejected at startup (TileProxyOptionsValidator) — the service never falls back to a hardcoded public log. See “Configuration” below anddevops/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
- Tile Proxying: Forward tile requests to upstream Rekor, caching responses locally
- Content-Addressed Storage: Store tiles by hash for deduplication and immutability
- TUF Integration: Optionally validate metadata using TUF trust anchors
- Request Coalescing: Deduplicate concurrent requests for the same tile
- Checkpoint Caching: Cache and serve recent checkpoints
- Offline Mode: Serve from cache when upstream is unavailable
API Surface
Proxy Endpoints (Passthrough)
| Endpoint | Description |
|---|---|
GET /tile/{level}/{index} | Proxy tile request (cache-through) |
GET /tile/{level}/{index}.p/{partialWidth} | Proxy partial tile |
GET /checkpoint | Proxy checkpoint request |
GET /api/v1/log/entries/{uuid} | Proxy entry lookup |
Admin Endpoints
| Endpoint | Description |
|---|---|
GET /_admin/cache/stats | Cache statistics (hits, misses, size) |
GET /_admin/metrics | JSON counter snapshot (see Metrics) |
POST /_admin/cache/sync | Returns Sync job queued and logs the request; it does not start a sync (the handler is a stub) |
DELETE /_admin/cache/prune | Prune old tiles |
GET /_admin/health | Health check |
GET /_admin/ready | Readiness 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
- LRU by Access Time: Least recently accessed tiles evicted first
- Max Size Limit: Configurable maximum cache size
- TTL Override: Force re-fetch after configurable time (for checkpoints)
- 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:
- Load service map from TUF to discover Rekor URL
- Validate Rekor public key from TUF targets
- Verify checkpoint signatures using TUF-loaded keys
- 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:
- Try primary upstream first
- On timeout/error, try next upstream
- Cache successful source for subsequent requests
- Reset failover state on explicit refresh
Deployment Model
One service, two ways to connect it
The stack runs one tile proxy: attestor-tileproxy (stellaops/attestor-tileproxy), defined in devops/compose/docker-compose.stella-services.yml. It ships with no upstream and a tmpfs cache. There are two ways to connect it to a log:
- Environment only. Set
ATTESTOR_TILEPROXY_UPSTREAM_URL,ATTESTOR_TILEPROXY_ORIGINandATTESTOR_TILEPROXY_SYNC_ENABLED(empty/false by default). The cache stays in the tmpfs. - The opt-in overlay
devops/compose/docker-compose.tile-proxy.yml. It configures the sameattestor-tileproxyservice; it does not start a second proxy. It requiresTILE_PROXY_UPSTREAM_URL(compose refuses to render without it), replaces the tmpfs with the named volumestile-cache(/var/cache/stellaops/tiles) andtuf-cache(/var/cache/stellaops/tuf), publishes${TILE_PROXY_PORT:-8090}:8080, and sets the TUF, cache, sync and request settings fromTILE_PROXY_*variables.
TILE_PROXY_UPSTREAM_URL=https://rekor.sigstore.dev TILE_PROXY_ORIGIN='rekor.sigstore.dev - 1985497715' docker compose -f docker-compose.stella-ops.yml -f docker-compose.tile-proxy.yml up -d
Until 2026-09-27 the overlay defined its own tile-proxy service built from the same project, so enabling it ran two copies of one deployable (SPRINT_20260927_002 PCD-72).
Metrics
GET /_admin/metrics returns a JSON snapshot, not a Prometheus exposition: cacheHits, cacheMisses, hitRatePercent, upstreamRequests, upstreamErrors, inflightRequests (Endpoints/TileEndpoints.cs, MetricsResponse). No Prometheus meter is registered by the host.
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 (noAddAuthentication/UseAuthorization, no client-cert validation, no rate limiter inProgram.cs). The entire/_admingroup is anonymous — and it is not read-only:DELETE /_admin/cache/prunedeletes 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.
- No Authentication (current behaviour, not just a default): intended for internal-network use behind the router; the
/_admingroup (incl. state-mutatingsync/prune) is unauthenticated. - Optional mTLS (NOT IMPLEMENTED — future): client-certificate validation is not wired in the TileProxy host.
- Rate Limiting (NOT IMPLEMENTED — future): no per-client-IP limiter is registered.
- Audit Logging (NOT IMPLEMENTED — future): cache operations are not emitted to an audit sink.
- Immutable Tiles: Full tiles are never modified after caching.
Error Handling
| Scenario | Behavior |
|---|---|
| No upstream configured (shipped default) | Serve from cache; on a miss return 503 upstream_not_configured. Never dials out. |
| Upstream unavailable | Serve from cache if available (marked stale); 502 otherwise |
| Invalid tile data | Reject, don’t cache, log error |
| Cache full | Evict LRU tiles, continue serving |
| TUF validation fails | Reject request, return 502 |
| Checkpoint stale | Refresh from upstream, warn in logs |
Future Enhancements
- Tile Prefetching: Prefetch tiles for known verification patterns
- Multi-Log Support: Support multiple transparency logs
- Replication: Sync cache between proxy instances
- Compression: Optional tile compression for storage
