tailscale-rss-processor — Design
1. Overview
- Purpose: Ingest Tailscale security bulletins from the public RSS feed into CVEMetadata.
- Source URL:
https://tailscale.com/security-bulletins/index.xml - Owner: Tailscale Inc.
- Licence: Public feed, no auth required.
- Schedule cadence: Runs weekly on Sundays at 06:00 UTC (
cron(0 6 ? * SUN *)). Low frequency appropriate for the infrequent update cadence (~2–3 bulletins per quarter). - ECS resources: cpu_units=256, memory_mb=512, expected_duration_minutes=30.
- Reads: External RSS feed, BulkDataDumpTracker (freshness), LoadProcessedHashes (resume).
- Writes: CVEMetadata, CVEDescription, CVEReference, CVEAlias, BulkDataDumpTracker, S3 archive.
2. Source contract (from Phase 0 recon)
| Property | Finding |
|---|---|
| Cadence | ~2–3 bulletins per quarter (low) |
| Volume | 36 bulletins total (TS-2022-001 through TS-2026-001) |
| Identifier | TS-YYYY-NNN (source-prefixed); some bulletins carry CVE aliases |
| Backfillability | Full historical archive available in single RSS feed |
| Anti-bot / auth | None — fully public, no rate-limiting observed |
| Pagination | Single RSS feed, no pagination |
| Freshness signal | lastBuildDate in RSS channel; tracker SHA256 of full XML body |
CVE presence verified by grep:
curl -sS https://tailscale.com/security-bulletins/index.xml | grep -oE 'CVE-[0-9]{4}-[0-9]{3,7}' | sort -u
# CVE-2022-41924
# CVE-2022-41925
# CVE-2023-28436
3. Architecture diagram
4. Source → DB field mapping
| Source field | Target table / column | Notes |
|---|---|---|
<title> TS-ID | CVEMetadata.cveId | Only when no CVE alias; see §5 |
<title> TS-ID | CVEAlias.aliasCveId | When CVE is primary cveId |
| Extracted CVE ID | CVEMetadata.cveId | Preferred when present |
| Extracted CVE ID | CVEAlias.aliasCveId | When TS-ID is primary cveId |
<pubDate> | CVEMetadata.datePublished | RFC 2822 → Unix seconds |
| Description summary | CVEMetadata.title | Human-readable line after “Description: " |
<link> | CVEMetadata.sourceAdvisoryRef | Bulletin anchor URL |
| Plain text description | CVEDescription.value | HTML stripped, entities decoded |
<link> | CVEReference.url | type="advisory" |
| SHA1(guid|pubDate|description) | CVEMetadata.sourceFileHash | Drift detection |
| Full parsed advisory JSON | CVEMetadata.rawDataJSON | Includes sections map |
5. Identifier policy
| Bulletin has CVE? | CVEMetadata.cveId | CVEAlias rows |
|---|---|---|
| Yes | CVE-YYYY-NNNN (preferred — maximises downstream hit-rate) | TS-YYYY-NNN |
| No | TS-YYYY-NNN | Any future CVE references discovered later |
Bundle suppression: N/A — Tailscale bulletins are 1:1 (one TS-ID per bulletin, at most one CVE per bulletin). No multi-CVE bundles.
6. CRIT / VEX
Not applicable. Tailscale bulletins describe software vulnerabilities in Tailscale’s own products, not cloud-resource (Provider, Service, ResourceType) triples.
7. S3 / source-file archive layout
- Bucket:
$S3_BUCKET_NAME - Archive prefix:
{source}/files/{sha256}/{cveId}.json - Quarantine prefix:
failed-feeds/{processorName}/{date}/{reason}/{filename} - Payload schema: Canonical JSON of the parsed
Advisorystruct (stable filename ={cveId}.json)
8. Error handling & Slack
| Event | Condition |
|---|---|
Started | Every run |
Completed | Clean exit (zero errors) |
Errored | Fatal fetch/parse failure, or one or more per-item store failures |
NoWork | Tracker fresh (within frequencySecs) and --force=false |
Per-record retry: 3 attempts via db.WithTx (implicit retry at connection level). No savepoints — low volume makes full-batch retry acceptable.
Retry profile: Transient connection errors (unexpected EOF, connection reset) are retried by pgx pool. Fatal errors (parse failure, constraint violation) fail fast.
9. Performance
- Concurrency: Sequential — 36 total items, no fan-out needed.
- Per-request timeout: 30 seconds for HTTP fetch.
- Rate-limit pacing: None required — public feed, low frequency.
- Soft deadline:
EXPECTED_DURATION_MINUTES - 10minutes; checked before dispatching each item. - Resume strategy:
db.LoadProcessedHashes(cveId → sourceFileHash) loaded once at startup. Items with unchanged hash are skipped.
10. Backfill
No separate backfill binary warranted — the full historical archive fits in a single RSS feed. The scheduled binary with --force=true processes all bulletins.
Justfile recipe (already exists):
go-tailscale-rss-backfill TARGET="local" ALL="false" LIMIT="0":
...
Expected runtime on prod data: < 1 minute for 36 bulletins.
S3 Persistence
Not used. This processor does not currently archive payloads or quarantine failures to S3. Per the S3 Persistence Contract this is non-compliant — see the compliance matrix for the implementation roadmap.
Expected paths when implemented:
- Archive:
tailscale/files/{sha256}/{filename} - Quarantine:
failed-feeds/tailscale-rss-processor/{YYYY-MM-DD}/{reason}/{filename} - Likely reasons:
parse-error