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)

PropertyFinding
Cadence~2–3 bulletins per quarter (low)
Volume36 bulletins total (TS-2022-001 through TS-2026-001)
IdentifierTS-YYYY-NNN (source-prefixed); some bulletins carry CVE aliases
BackfillabilityFull historical archive available in single RSS feed
Anti-bot / authNone — fully public, no rate-limiting observed
PaginationSingle RSS feed, no pagination
Freshness signallastBuildDate 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

graph LR Source[RSS Feed] --> HTTP[httpclient.New] --> Parse[tailscale.ParseFeed] Parse --> Map[tailscale.MapToSourceData] Map --> Tx[(pgx Tx)] --> Pipeline[processor.StoreCVESourceData] Pipeline --> CVEMetadata Pipeline --> CVEDescription Pipeline --> CVEReference Pipeline --> CVEAlias[(db.InsertAliases)] Tx -.commit.-> S3[s3client.ArchiveRecord]

4. Source → DB field mapping

Source fieldTarget table / columnNotes
<title> TS-IDCVEMetadata.cveIdOnly when no CVE alias; see §5
<title> TS-IDCVEAlias.aliasCveIdWhen CVE is primary cveId
Extracted CVE IDCVEMetadata.cveIdPreferred when present
Extracted CVE IDCVEAlias.aliasCveIdWhen TS-ID is primary cveId
<pubDate>CVEMetadata.datePublishedRFC 2822 → Unix seconds
Description summaryCVEMetadata.titleHuman-readable line after “Description: "
<link>CVEMetadata.sourceAdvisoryRefBulletin anchor URL
Plain text descriptionCVEDescription.valueHTML stripped, entities decoded
<link>CVEReference.urltype="advisory"
SHA1(guid|pubDate|description)CVEMetadata.sourceFileHashDrift detection
Full parsed advisory JSONCVEMetadata.rawDataJSONIncludes sections map

5. Identifier policy

Bulletin has CVE?CVEMetadata.cveIdCVEAlias rows
YesCVE-YYYY-NNNN (preferred — maximises downstream hit-rate)TS-YYYY-NNN
NoTS-YYYY-NNNAny 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 Advisory struct (stable filename = {cveId}.json)

8. Error handling & Slack

EventCondition
StartedEvery run
CompletedClean exit (zero errors)
ErroredFatal fetch/parse failure, or one or more per-item store failures
NoWorkTracker 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 - 10 minutes; 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