Project Tycho is an automated research platform for observing PQC adoption in TLS.
Project Tycho is a four-part research platform that for observing PQC adoption in TLS. The platform combines empirical measurement, structured analysis, publication-ready visualization, and experimental autonomous research generation around the theme of post-quantum cryptography (PQC) in TLS.
A command-line / library tool that parses pcap files and extracts structured data about TLS handshakes, with particular focus on PQC cipher suites, key exchange parameters, and signature algorithms.
A FastAPI + Vue.js web app that consumes the analyzer's output, renders interactive visualizations of TLS handshakes, and exports publication-ready PGF/TikZ source for LaTeX.
An orchestration service that periodically scans a curated list of target websites, records their TLS handshakes as pcaps, invokes the analyzer, and updates a live adoption dashboard.
The researcher/ tool is an AI-powered agent (LangChain + LangGraph) that combines book theory, empirical Observatory data, and Visualiser-generated assets to draft short research summaries, article drafts, and blog posts about PQC adoption in TLS.
Current scope is a CLI one-shot MVP (no built-in scheduler or long-running API service yet).
The blog/ service is a static site generator and HTTP API that accepts research posts from the Researcher agent, converts Markdown to HTML via Jinja2 templates, and serves the resulting blog site. Posts are stored as JSON files in a persistent volume and rendered on demand.
The first three tools compose into a single continuous pipeline on one VPS. The fourth is a deliberate experiment in AI-augmented research workflows. The fifth serves as the content publication layer.
Two Observatory behaviors are important when interpreting current results. These are current implementation limitations, not intended guarantees:
- Manual single-group probes are not persisted. Running
observatory scan HOST --openssl-groups GROUPprints the probe result to stdout, but it does not append a scan row to the Observatory JSON store. These ad-hoc probes therefore do not appear in status output, scan history, Visualiser dashboards, or adoption calculations. - Per-host probe results are persisted only after that host's full group sequence finishes. During scheduled rounds, completed probes for a target are kept in memory until all locally supported groups for that target have been attempted. If the process stops mid-target, those completed probes are lost from the JSON history; if an individual write later fails, the stored round can be incomplete and the current reporting layer cannot distinguish that from a complete round.
Quick start:
cd researcher
uv sync --extra dev
uv run researcher run \
--topic "Weekly PQC TLS adoption update" \
--output-type research-summaryThe fastest way to run the Observatory and Visualiser together is with a single command from the repository root:
docker compose upThis builds and starts:
- Observatory – scans TLS handshakes on a weekly schedule and writes results to a shared volume (
observatory_data). RequiresNET_RAWcapability sotcpdumpcan capture packets. - Visualiser – serves the interactive dashboard at http://localhost:8000. The Vue.js frontend is built automatically before the backend starts.
- Blog – serves the research blog at http://localhost:8001. The Researcher agent publishes posts here.
To also run the one-shot Researcher agent (requires an OpenAI API key):
OPENAI_API_KEY=sk-... docker compose --profile research run --rm researcherOverride the topic or output type via environment variables:
OPENAI_API_KEY=sk-... \
RESEARCHER_TOPIC="Monthly PQC digest" \
RESEARCHER_OUTPUT_TYPE=blog-post \
docker compose --profile research run --rm researcherWhen running from the root docker-compose.yml, keep service settings in a single repository-root .env file. Docker Compose reads this file and passes the relevant values to each service.
The Researcher agent can auto-generate and publish a blog post covering the last 7 days of Observatory data to the Blog service. First ensure all services are running:
docker compose up -dThen trigger a weekly blog post generation:
OPENAI_API_KEY=sk-... docker compose --profile research run --rm researcher blog-weeklyThis command:
- Collects the last 7 days of TLS scan data from the Observatory
- Retrieves matching reference snippets from the mounted
.tex/.md/.txtfiles - Generates visual TikZ assets via the Visualiser
- Drafts, critiques, and finalizes a blog-formatted Markdown post
- Publishes the post to the Blog service via
POST /api/posts - Triggers a static site rebuild via
POST /api/rebuild
The blog is available at http://localhost:8001.
On a VPS, schedule it with cron to run every Monday morning (after the Sunday Observatory scan):
0 9 * * 1 cd /srv/project-tycho && OPENAI_API_KEY=sk-... docker compose --profile research run --rm researcher blog-weekly
Create a system prompt file (see system-prompts/blog-prompt.txt for an
example) and mount it:
OPENAI_API_KEY=sk-... \
RESEARCHER_BLOG_SYSTEM_PROMPT_FILE=/var/pqc-obs/system-prompts/blog-prompt.txt \
docker compose --profile research run --rm researcher blog-weeklyThe default mount is ./system-prompts:/var/pqc-obs/system-prompts:ro, so
editing system-prompts/blog-prompt.txt in the repository is enough.
The simplest production layout on a Hetzner Ubuntu VPS is:
- PCAP Analyser installed as a local CLI that the Observatory can call
- Visualiser running as a
systemdservice behind Nginx - Observatory running as a long-lived
systemdservice
The steps below assume:
- Ubuntu 24.04 on the VPS
- a public DNS name such as
tycho.example.com - the repository checked out to
/srv/project-tycho - a non-root deployment user, for example
deploy
sudo apt update
sudo apt install -y \
git curl nginx tcpdump \
python3 python3-venv python3-pip \
nodejs npm
curl -LsSf https://astral.sh/uv/install.sh | sh
source "$HOME/.local/bin/env"tcpdump needs elevated packet-capture privileges, so the Observatory service below
is configured with CAP_NET_RAW.
Because CAP_NET_RAW allows raw packet capture, keep this VPS limited to this
workload or otherwise isolate the service carefully with additional hardening
such as AppArmor/SELinux policy, a dedicated deployment user, or container /
VM isolation.
For production, review the uv installer script before executing it.
Create the deployment user if you do not already have one:
sudo adduser deploy
sudo usermod -aG sudo deployClone the repository:
sudo mkdir -p /srv
sudo chown "$USER":"$USER" /srv
git clone https://github.com/duplys/project-tycho.git /srv/project-tycho
cd /srv/project-tychoThe PCAP Analyser is best deployed as a local command that the Observatory can execute directly.
cd /srv/project-tycho/pcap-analyser
uv syncTest it:
cd /srv/project-tycho/pcap-analyser
uv run tls-pcap-analyzer --helpFor integration with the Observatory, use the absolute analyzer path:
/srv/project-tycho/pcap-analyser/.venv/bin/tls-pcap-analyzer
Build the frontend and install the backend:
cd /srv/project-tycho/visualiser
uv sync
cd /srv/project-tycho/visualiser/frontend
npm install
npm run buildCreate a systemd unit at /etc/systemd/system/tycho-visualiser.service:
[Unit]
Description=Project Tycho Visualiser (TLS Visualizer)
After=network.target
[Service]
User=deploy
Group=deploy
WorkingDirectory=/srv/project-tycho/visualiser
Environment=PATH=/srv/project-tycho/visualiser/.venv/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/srv/project-tycho/visualiser/.venv/bin/uvicorn tls_visualizer.app:app --host 127.0.0.1 --port 8000
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetEnable it:
sudo systemctl daemon-reload
sudo systemctl enable --now tycho-visualiser
sudo systemctl status tycho-visualiserInstall the application:
cd /srv/project-tycho/observatory
uv sync
sudo mkdir -p /var/pqc-obs/captures
sudo chown deploy:deploy /var/pqc-obs/capturesCreate /srv/project-tycho/observatory/.env:
OBSERVATORY_PCAP_DIR=/var/pqc-obs/captures
OBSERVATORY_STORAGE_FILE=/var/pqc-obs/data/observatory-data.json
OBSERVATORY_SCAN_SCHEDULE_DAY_OF_WEEK=sun
OBSERVATORY_SCAN_SCHEDULE_HOUR=8
OBSERVATORY_SCAN_SCHEDULE_MINUTE=0
OBSERVATORY_SCAN_SCHEDULE_TIMEZONE=Europe/BerlinRestrict the file so only the deployment user can read it:
chmod 600 /srv/project-tycho/observatory/.envInitialize the schema and targets:
cd /srv/project-tycho/observatory
uv run observatory initCreate a systemd unit at /etc/systemd/system/tycho-observatory.service:
[Unit]
Description=Project Tycho Observatory (PQC Observatory)
After=network.target
[Service]
User=deploy
Group=deploy
WorkingDirectory=/srv/project-tycho/observatory
Environment=PATH=/srv/project-tycho/observatory/.venv/bin:/usr/local/bin:/usr/bin:/bin
# Required so tcpdump can capture packets without running the whole service as root.
AmbientCapabilities=CAP_NET_RAW
CapabilityBoundingSet=CAP_NET_RAW
NoNewPrivileges=true
ExecStart=/srv/project-tycho/observatory/.venv/bin/observatory start
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetEnable it:
sudo systemctl daemon-reload
sudo systemctl enable --now tycho-observatory
sudo systemctl status tycho-observatoryCreate /etc/nginx/sites-available/vier99.de:
Replace tycho.example.com with your real domain name.
server {
listen 80;
server_name tycho.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Enable the site:
sudo ln -s /etc/nginx/sites-available/vier99.de /etc/nginx/sites-enabled/vier99.de
sudo nginx -t
sudo systemctl reload nginxThen add HTTPS with Let's Encrypt if the domain is public:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d tycho.example.comUseful commands after deployment:
# Visualiser logs
sudo journalctl -u tycho-visualiser -f
# Observatory logs
sudo journalctl -u tycho-observatory -f
# Observatory status
cd /srv/project-tycho/observatory
uv run observatory status
# One manual scan
uv run observatory scan cloudflare.comcd /srv/project-tycho
git pull
cd /srv/project-tycho/pcap-analyser && uv sync
cd /srv/project-tycho/visualiser && uv sync
cd /srv/project-tycho/visualiser/frontend && npm install && npm run build
cd /srv/project-tycho/observatory && uv sync
sudo systemctl restart tycho-visualiser
sudo systemctl restart tycho-observatoryIf you prefer containers over a native install, docker-compose.yml at the repository root is the only runtime dependency you need on the VPS — no Python, Node.js, or uv required on the host.
The steps below assume:
- Ubuntu 24.04 on the VPS
- a public DNS name such as
tycho.example.com - the repository checked out to
/srv/project-tycho
Install Git and Docker Engine with the Compose plugin:
sudo apt update && sudo apt install -y gitFollow the official Docker documentation to install Docker Engine and the Compose plugin (docker compose).
Clone the repository:
sudo mkdir -p /srv
sudo chown "$USER":"$USER" /srv
git clone https://codeberg.org/TLS-Port/project-tycho.git /srv/project-tycho
cd /srv/project-tychoCreate a repository-root .env file alongside docker-compose.yml to override defaults for any service:
OBSERVATORY_SCAN_SCHEDULE_DAY_OF_WEEK=sun
OBSERVATORY_SCAN_SCHEDULE_HOUR=8
OBSERVATORY_SCAN_SCHEDULE_MINUTE=0
OBSERVATORY_SCAN_SCHEDULE_TIMEZONE=Europe/Berlin
# OPENAI_API_KEY=sk-...This example schedules the Observatory for Sunday morning at 08:00 in Berlin
local time. The Europe/Berlin timezone keeps the scan at local 08:00 across
summer/winter daylight saving time changes.
Restrict the file so only the deployment user can read it:
chmod 600 /srv/project-tycho/.envAll services have sensible defaults except the optional Researcher API key; this step can be skipped for a quick start. See §4 Install Observatory for the full list of Observatory variables.
If you want to run the one-shot Researcher agent from Docker Compose, add the API key to the same repository-root .env file:
OPENAI_API_KEY=sk-...Docker Compose automatically reads this file when evaluating ${OPENAI_API_KEY} in docker-compose.yml, so you do not need to export the variable manually for each run.
cd /srv/project-tycho
docker compose up -dThis builds images from the repository and starts:
- Observatory – runs the periodic TLS scanner. Pcap captures and scan results are written to Docker-managed named volumes (
pcap_store,observatory_data). GrantedNET_RAWcapability sotcpdumpcan capture packets without running as root. - Visualiser – serves the interactive dashboard at http://localhost:8000. The Vue.js frontend is built automatically by the
visualiser-frontendhelper service before the backend starts. - Blog – serves the research blog at http://localhost:8001. Posts are published by the Researcher agent.
Verify both services are up:
docker compose psThe Visualiser binds to port 8000 and the Blog to port 8001 on the host. Install Nginx with:
sudo apt install -y nginx certbot python3-certbot-nginxAdd a server block to /etc/nginx/sites-available/vier99.de
(replace tycho.example.com with your real domain). Both the Visualiser
and Blog must live in the same server block — two separate blocks for
the same server_name means nginx only routes to the first one.
# Project Tycho
server {
server_name tycho.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /blog/ {
rewrite ^/blog(/.*)$ $1 break;
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/tycho.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tycho.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}If Certbot has not yet issued a certificate for this domain, run:
sudo certbot --nginx -d tycho.example.comDo not reuse another site's certificate path unless that certificate already
covers project-tycho.vier99.de as a SAN name.
For the first issuance, make sure Nginx can pass nginx -t before running
Certbot. If the project-tycho HTTPS block points at a cert that does not
exist yet, temporarily remove that block or point it at an existing cert until
the new one is created.
Create a dedicated certificate with:
sudo certbot --nginx --cert-name project-tycho.vier99.de -d project-tycho.vier99.deEnable the site and add HTTPS with Let's Encrypt:
sudo apt install -y nginx certbot python3-certbot-nginx
sudo ln -s /etc/nginx/sites-available/vier99.de /etc/nginx/sites-enabled/vier99.de
sudo nginx -t && sudo systemctl reload nginx# Follow logs for all services
docker compose -f /srv/project-tycho/docker-compose.yml logs -f
# Follow a single service
docker compose -f /srv/project-tycho/docker-compose.yml logs -f observatory
docker compose -f /srv/project-tycho/docker-compose.yml logs -f visualiser
docker compose -f /srv/project-tycho/docker-compose.yml logs -f blog
# Run the one-shot Researcher agent
OPENAI_API_KEY=sk-... docker compose -f /srv/project-tycho/docker-compose.yml \
--profile research run --rm researcher
# Publish a weekly blog post
OPENAI_API_KEY=sk-... docker compose -f /srv/project-tycho/docker-compose.yml \
--profile research run --rm researcher blog-weeklyThe Observatory stores Tool 1 (PCAP Analyser) output inside the shared JSON data file. You can extract a single handshake record and upload it to the Visualiser frontend for interactive analysis and TikZ export.
Export the latest successful scan as a standalone JSON file:
cd /srv/project-tycho
docker compose exec -T observatory python - <<'PY' > /srv/project-tycho/tool1-latest.json
import json
p = "/var/pqc-obs/data/observatory-data.json"
d = json.load(open(p))
for scan in reversed(d.get("scans", [])):
ao = scan.get("analyzer_output")
if scan.get("error") is None and ao:
print(json.dumps(ao, indent=2))
break
else:
raise SystemExit("No successful scan with analyzer_output found")
PYVerify the file was created:
ls -lah /srv/project-tycho/tool1-latest.json
head -n 30 /srv/project-tycho/tool1-latest.jsonCopy the file to your local machine:
scp root@tycho.vier99.de:/srv/project-tycho/tool1-latest.json .Upload to Visualiser:
- Open the Visualiser dashboard at https://tycho.vier99.de
- Click "Load" under "Load TLS Handshake Data"
- Select the downloaded
tool1-latest.jsonfile - Click the "Load" button
- Click "⬇ Handshake Flow (.tex)" to download the LaTeX source
- Compile with
pdflatex— cipher suite names are now properly escaped (underscores become\_)
Export a specific hostname's latest successful scan:
cd /srv/project-tycho
docker compose exec -T observatory python - <<'PY' > /srv/project-tycho/tool1-cloudflare.json
import json
hostname = "cloudflare.com"
p = "/var/pqc-obs/data/observatory-data.json"
d = json.load(open(p))
target_id = None
for t in d.get("targets", []):
if t["hostname"] == hostname:
target_id = t["id"]
break
if not target_id:
raise SystemExit(f"Target {hostname} not found")
for scan in reversed(d.get("scans", [])):
if scan["target_id"] == target_id:
ao = scan.get("analyzer_output")
if scan.get("error") is None and ao:
print(json.dumps(ao, indent=2))
break
else:
raise SystemExit(f"No successful scan with analyzer_output for {hostname}")
PYThis exports only the latest successful capture for that hostname.
Pull the latest code, rebuild images, and restart the stack:
cd /srv/project-tycho
git pull
docker compose up -d --buildClean up dangling images after the update:
docker image prune -f