filosottile

Activity

Stars and mentions · Last 30 days, Oct 9 – Nov 7

GitHub stars169
Mentions13
036912Oct 9Oct 23Nov 7
UTC days · repositories filosottile owns12 repositories

Top repositories

filosottile’s repositories ranked by new forks, as on trending.

Loading

Contributes to

Repositories outside filosottile’s account where filosottile opened or reviewed pull requests in the past 365 days.

Who noticed filosottile

Everyone who starred or forked filosottile’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

14posts
Nov 81 post
  1. バックアップは「避難訓練」をしないと戻らない。「おはしも」で覚えるリストア訓練【MySQL・S3 Object Lock・age】 「訓練、訓練。ただいま、理科室から火災が発生しました」 小学生のころ、この放送が流れると、教室の空気が少しだけ浮き立ちました。防災頭巾をかぶって、先生のあとについて廊下を歩いて、校庭に体育座りで並ぶ。誰かが必ず笑って、誰かが必ず怒られる。最後に校長先生が朝礼台に上がって、「今日は〇分〇秒で避難できました」と言う。 あのころは、どうして毎年同じことをやるのか分かりませんでした。火事の起きた学校なんて、見たこともなかったので。 明日、11月9日から15日までは秋季全国火災予防運動です。今年の統一標語は「火の確認 いい日を支える いい習慣」。それに合わせて、今日はサーバーの避難訓練の話を書きます。つまり、バックアップを実際に戻してみる訓練の話です。 「バックアップはあります」のその先 10月7日の未明、IDCフロンティアの IDCF クラウドで、東日本リージョン1の4つのゾーンがランサムウェアの被害に遭いました。影響は495の企業・自治体に及んでいます。翌日の発表には、「データの復元はお客さま自身が保持しているバックアップデータからのみ可能」という一文がありました。その後、スナップショットからの取り出しは段階的に進みましたが、初動の時点で頼れたのは、自分たちで外に持っていたバックアップだけだったわけです。 報道を追っていると、明暗がはっきり分かれていました。別のクラウドに世代バックアップを置いていた会社は、翌日の夜には別の事業者の上でサービスを仮復旧させていました。逆に、バックアップ用のサーバーを本番と同じ基盤に置いていたところは、バックアップごと使えなくなっています。 警察庁がまとめた令和7年のランサムウェア被害の統計にも、似た傾向が出ています。被害に遭った組織の約9割はバックアップを取っていました。ところが、そこから元どおりに戻せた組織は、約2割にとどまります。 「バックアップはありますか」と聞かれて「あります」と答えられる会社は多いはずです。でも、それだけでは足りません。Joel Spolsky が2009年に書いたブログに、こんな一文があります。 The minimum bar for a reliable service is not that you have done a backup, but that you have done a restore. (信頼できるサービスの最低ラインは、バックアップを取ったことではない。リストアをしたことだ) この記事では、小学校で習った避難の合い言葉「おはしも」(押さない・走らない・しゃべらない・戻らない)を借りて、リストア訓練の要点を4つにまとめます。地域によっては「おかしも」(か=駆けない)と習った方もいると思います。どちらでも中身は同じです。 最後に、毎晩のバックアップと月1回の訓練をそのまま回せるスクリプトを置いておきます。想定している構成は、中小規模の会社によくある「VPS 1台に Web アプリと MySQL、バックアップは S3」です。 訓練の前に:「どこまで」と「何分で」を決めておく 避難訓練にも「ねらい」があったように、リストア訓練にも合格ラインが要ります。業界では RPO・RTO と呼ばれますが、お客様と話すときには、私はこう言い換えています。 RPO:どこまで戻れば許せますか。(昨日の夜の状態まで戻れば OK? 1時間前まで必要?) RTO:何時間で戻ればいいですか。(半日止まっても大丈夫? 2時間が限界?) 予約サイトなら「昨夜の状態」では足りないことが多いです。今朝入った予約が消えてしまうので。逆に、更新が週1回のコーポレートサイトなら、1週間前の状態でも困らないかもしれません。 この2つの数字が決まると、バックアップの頻度も、置き場所も、訓練で測るべきものも、自然に決まります。逆に、ここが決まっていない訓練は「とりあえず戻った、よかったね」で終わってしまいます。校長先生が「〇分〇秒でした」と言えたのは、目標タイムがあったからです。 「お」 おさない ― 本番を押しのけて戻さない 訓練でいちばんやってはいけないのは、本番の上にリストアすることです。押し合って将棋倒しになるのと同じで、訓練で本番を壊したら本末転倒です。 戻す先は、使い捨ての別環境にします。後半のスクリプトでは、Docker の MySQL コンテナを --network none(ネットワークなし)で立ち上げて、そこへ流し込んでいます。 ネットワークを切るのには理由があります。以前、あるサイトのステージング環境に本番のデータを戻したら、予約リマインドの cron が動いて、本物のお客様にメールが飛んでしまった、という話を聞いたことがあります。復元したデータには、本物のメールアドレスと、本物の「明日10時のご予約」が入っています。アプリまで起動して画面を確かめるなら、メール送信は MAIL_MAILER=log のように外へ出ない設定にして、cron とキューワーカーは止めておきます。 もう一つ、訓練用の環境に戻したデータは本物の個人情報です。訓練に使う PC はディスクを暗号化しておいて、終わったら復号したファイルを必ず消します。訓練のつもりで情報漏えいを起こしたら、笑い話にもなりません。 「は」 はしらない ― 鍵と手順書を、燃える建物の外に 慌てて走ると転びます。障害の当日、焦っている人間が頼れるのは、手順書と、前に一度やったという記憶だけです。 そして訓練をやると、たいてい最初にここでつまずきます。 「バックアップの暗号化キー、どこにありましたっけ」 バックアップを暗号化するのは正しい判断です。でも、その鍵が本番サーバーの中にしかなかったら、サーバーが燃えた時点で、バックアップは誰にも開けない金庫になります。開けられないのは攻撃者も同じなので、ある意味では最強のセキュリティかもしれませんが。 この記事のスクリプトでは、暗号化に age を使っています。age は公開鍵で暗号化して、秘密鍵で復号する仕組みなので、サーバーには公開鍵だけを置いて、秘密鍵はサーバーの外(パスワードマネージャーや、訓練用 PC)に置いておけます。サーバーを乗っ取られても、攻撃者は過去のバックアップを読めません。 同じように、建物の外に置いておくべきものがいくつかあります。災害でいう「非常持ち出し袋」です。 バックアップの復号キー バックアップ置き場にログインするための認証情報(本番とは別のもの) .env など、アプリの設定ファイル一式 crontab や systemd タイマーの中身 ドメインのレジストラと DNS の管理画面のログイン情報、二段階認証の復旧コード この手順書そのもの 最後の項目は冗談のようですが、手順書を本番サーバーの Wiki に置いていて、障害の日に読めなかった、という話は本当にあります。 「し」 しゃべらない ― 本番とバックアップに、会話をさせない 避難訓練の「しゃべらない」は、先生の指示を聞き取るための約束でした。サーバーの世界では、少し意味を変えて使います。本番とバックアップのあいだで、余計な会話をさせないことです。 ランサムウェアの攻撃者は、暗号化の前にバックアップを探して消しに来ます。そのとき、本番サーバーの中にバックアップを消せる認証情報が置いてあったら、それで終わりです。 そこで、本番サーバーに渡す権限は「置く」だけにします。 backup-writer-policy.json { "Version": "2012-10-17", "Statement": [ { "Sid": "PutOnly", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::example-backup-bucket/*" } ] } 一覧も、読み出しも、削除もできません。本番サーバーにできるのは、新しいファイルを置いていくことだけです。 ただし PutObject だけでも、同じキーで上書きされると中身は消えます。そこで、バケット側に S3 Object Lock をかけておきます。これは本番とは別の管理用アカウントから、最初に一度だけ実行します。 管理用の端末で一度だけ aws s3api create-bucket --bucket example-backup-bucket \ --region ap-northeast-1 \ --create-bucket-configuration LocationConstraint=ap-northeast-1 \ --object-lock-enabled-for-bucket aws s3api put-object-lock-configuration --bucket example-backup-bucket \ --object-lock-configuration '{ "ObjectLockEnabled": "Enabled", "Rule": { "DefaultRetention": { "Mode": "COMPLIANCE", "Days": 35 } } }' Object Lock を有効にするとバージョニングも自動で有効になるので、上書きしても古い版は残ります。そして COMPLIANCE モードでは、保持期間のあいだは、AWS アカウントの root ユーザーでも削除できません。 ! COMPLIANCE モードは「誰にも消せない」モードなので、設定を間違えると、保持期間が終わるまで料金を払い続けることになります。最初は GOVERNANCE モード(特別な権限があれば解除できる)で動きを確かめて、日数に納得してから切り替えるのがおすすめです。保持期間を過ぎたものは、ライフサイクルルールで自動削除するようにしておきます。 置き場所は、本番とは別の事業者、少なくとも別のアカウントにします。IDCF の件で明暗を分けたのも、まさにここでした。同じ建物の中の金庫は、建物ごと燃えます。 「も」 もどらない ― 燃えている建物には戻らない 小学校の訓練で、忘れ物を取りに教室へ戻ろうとした同級生がいました。先生にものすごい勢いで止められていたのを覚えています。忘れ物は、たしか給食袋でした。 サーバーの障害でも同じことが起きます。乗っ取られたサーバーを「いまのうちに少しでもデータを」と触りに行ったり、同じサーバーの上にバックアップを戻して再開しようとしたりする。でも、侵入経路がふさがっていない建物に戻るのは、火の中に戻るのと同じです。戻したそばから、また暗号化されるかもしれません。 ランサムウェアのときは、別の場所に建て直すのが基本です。そのためには、ふだんから避難先を決めておく必要があります。 別の事業者の上に、同じ構成を立てる手順(できれば、構築スクリプトとして) DNS の切り替え手順と、TTL をどこまで下げられるか IP アドレスが変わったときに、許可リストを直してもらう必要がある連携先の一覧 SSL 証明書の再発行 もう一つの「戻らない」は、世代の話です。攻撃者は侵入してから数日〜数週間、静かに中を調べてから暗号化することがあります。最新のバックアップに、すでに攻撃者の置き土産が入っているかもしれません。毎日のバックアップを7日分、週1を4週分、月1を数か月分、というように世代を残しておくと、「侵入される前」まで戻れる可能性が上がります。先ほどの Object Lock の35日は、この「週1を4週分」をカバーするための数字です。 訓練の実施:毎晩のバックアップと、月1回の避難訓練 ここからは、手元の構成に合わせて書き換えればそのまま使える形のスクリプトです。 毎晩のバックアップ(本番サーバー側) ポイントは、ダンプの直前に、テーブルごとの件数とファイル数を記録しておくことです。訓練のときに、戻した結果と答え合わせをするためです。 backup.sh #!/usr/bin/env bash 毎晩 cron / systemd タイマーから実行する。 MySQL の接続情報は ~/.my.cnf(バックアップ専用ユーザー)に置いておく。 set -euo pipefail DB_NAME="app" APP_SHARED="/var/www/app/shared" # storage/ がある場所 BUCKET="s3://example-backup-bucket" AGE_RECIPIENT="age1xxxxxxxxxxxxxxxx" # 公開鍵だけをサーバーに置く PING_URL="hc-ping.com" # 死活監視(後述) STAMP="$(date -u +%Y%m%dT%H%M%SZ)" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT cd "$WORK" 1) ダンプの「前」に、テーブルごとの件数とファイル数を数えておく mysql -N -B -e "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" "$DB_NAME" | while read -r table _; do count="$(mysql -N -B -e "SELECT COUNT(*) FROM `$table`" "$DB_NAME")" printf '%s\t%s\n' "$table" "$count" done > tables.tsv find "$APP_SHARED/storage" -type f | wc -l > files.txt 2) ダンプして、圧縮して、暗号化する --single-transaction: InnoDB をロックせずに、一貫した断面を取る --set-gtid-purged=OFF: 別のサーバーへ戻すときに GTID でつまずかないように mysqldump --single-transaction --routines --triggers --events \ --hex-blob --default-character-set=utf8mb4 --set-gtid-purged=OFF \ "$DB_NAME" | gzip | age -r "$AGE_RECIPIENT" -o db.sql.gz.age tar -C "$APP_SHARED" -czf - storage | age -r "$AGE_RECIPIENT" -o storage.tar.gz.age sha256sum db.sql.gz.age storage.tar.gz.age > SHA256SUMS 3) 「置く」だけの権限でアップロード aws s3 cp "$WORK/" "$BUCKET/$STAMP/" --recursive --only-show-errors 4) 最後まで来られたら、報知器に「生きています」と伝える curl -fsS -m 10 --retry 3 "$PING_URL" > /dev/null set -o pipefail は外さないでください。これがないと、mysqldump が途中で失敗しても、後ろの gzip と age が成功するので、スクリプト全体は「成功」で終わります。中身が空っぽのバックアップが、毎晩きちんと届くことになります。 月1回の避難訓練(本番とは別の PC 側) 訓練用のスクリプトは、本番とは別の PC か、別の事業者の小さなサーバーで動かします。読み出し専用の認証情報と、age の秘密鍵は、こちらにだけ置きます。 restore_drill.py #!/usr/bin/env python3 """バックアップの避難訓練。最新のバックアップを使い捨てのMySQLに戻して、答え合わせをする。""" import gzip import hashlib import os import shutil import subprocess import sys import tarfile import tempfile import time from datetime import datetime, timezone from pathlib import Path BUCKET = "s3://example-backup-bucket" AGE_KEY = os.environ["DRILL_AGE_KEY"] # 秘密鍵のパス。本番サーバーには置かない MYSQL_IMAGE = "mysql:8.4" # 本番と同じバージョンにそろえる CONTAINER = "restore-drill" RPO_HOURS = 24 # どこまで戻れば許せるか RTO_MINUTES = 120 # 何分で戻せばいいか LOG = Path("drill_log.md") laps: list[tuple[str, float]] = [] problems: list[str] = [] def run(*cmd: str) -> str: return subprocess.run(cmd, check=True, text=True, capture_output=True).stdout def sql(query: str) -> str: return run("docker", "exec", "-e", "MYSQL_PWD=drill", CONTAINER, "mysql", "-uroot", "-N", "-B", "app", "-e", query).strip() def step(name: str): """ラップを取るための小さなタイマー""" class _Lap: def enter(self): self.t = time.monotonic() print(f"▶ {name}", flush=True) def exit(self, *exc): laps.append((name, time.monotonic() - self.t)) return _Lap() def latest_stamp() -> str: lines = run("aws", "s3", "ls", f"{BUCKET}/").splitlines() stamps = [l.split()[-1].rstrip("/") for l in lines if l.strip().startswith("PRE ")] if not stamps: sys.exit("バックアップが1つも見つかりません") return max(stamps) # 20261108T171500Z 形式なので、文字列の最大 = 最新 def sha256(path: Path) -> str: h = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(1 << 20), b""): h.update(chunk) return h.hexdigest() def within(expected: int, actual: int) -> bool: 件数はダンプの「直前」に数えているので、ぴったりは一致しない。1%か10件までは許す return abs(actual - expected) <= max(10, expected // 100) def drill(work: Path) -> str: stamp = latest_stamp() with step("ダウンロード"): run("aws", "s3", "cp", f"{BUCKET}/{stamp}/", str(work), "--recursive", "--only-show-errors") with step("チェックサム確認"): for line in (work / "SHA256SUMS").read_text().splitlines(): expected, name = line.split() if sha256(work / name) != expected: problems.append(f"チェックサム不一致: {name}") with step("復号"): for name in ("db.sql.gz", "storage.tar.gz"): run("age", "-d", "-i", AGE_KEY, "-o", str(work / name), str(work / f"{name}.age")) with step("MySQL起動"): run("docker", "run", "-d", "--rm", "--name", CONTAINER, "--network", "none", "-e", "MYSQL_ROOT_PASSWORD=drill", "-e", "MYSQL_DATABASE=app", MYSQL_IMAGE) 初期化中の一時サーバーはTCPを開かない。127.0.0.1でpingが通れば、本番の起動が終わっている for _ in range(90): ping = subprocess.run(["docker", "exec", CONTAINER, "mysqladmin", "ping", "-h127.0.0.1", "--silent"], capture_output=True) if ping.returncode == 0: break time.sleep(2) else: sys.exit("MySQLが起動しません") with step("インポート"): with gzip.open(work / "db.sql.gz", "rb") as src: p = subprocess.Popen(["docker", "exec", "-i", "-e", "MYSQL_PWD=drill", CONTAINER, "mysql", "-uroot", "app"], stdin=subprocess.PIPE) shutil.copyfileobj(src, p.stdin) p.stdin.close() if p.wait() != 0: sys.exit("インポートに失敗しました") with step("答え合わせ"): for line in (work / "tables.tsv").read_text().splitlines(): table, expected = line.split("\t") try: actual = int(sql(f"SELECT COUNT(*) FROM {table}")) except subprocess.CalledProcessError: problems.append(f"{table}: テーブルがありません") continue if int(expected) > 0 and actual == 0: problems.append(f"{table}: 空っぽです(バックアップ時 {expected} 件)") elif not within(int(expected), actual): problems.append(f"{table}: {expected} 件 → {actual} 件") with tarfile.open(work / "storage.tar.gz", "r:gz") as tf: files = sum(1 for m in tf if m.isfile()) expected_files = int((work / "files.txt").read_text()) if not within(expected_files, files): problems.append(f"ファイル数: {expected_files} → {files}") taken = datetime.strptime(stamp, "%Y%m%dT%H%M%SZ").replace(tzinfo=timezone.utc) age_h = (datetime.now(timezone.utc) - taken).total_seconds() / 3600 if age_h > RPO_HOURS: problems.append(f"最新のバックアップが {age_h:.0f} 時間前です(目標 {RPO_HOURS} 時間)") return stamp def main() -> None: 復号したファイルは本物の個人情報。終わったら一時ディレクトリごと消える with tempfile.TemporaryDirectory(prefix="drill-") as tmp: try: stamp = drill(Path(tmp)) finally: subprocess.run(["docker", "stop", CONTAINER], capture_output=True) total_min = sum(sec for _, sec in laps) / 60 if total_min > RTO_MINUTES: problems.append(f"復元に {total_min:.0f} 分かかりました(目標 {RTO_MINUTES} 分)") print("\n―― 訓練結果 ――") for name, sec in laps: print(f" {name:<10} {sec:7.1f} 秒") print(f" 合計 {total_min:.1f} 分") for p in problems: print(f" ✗ {p}") print(" 判定:", "要改善" if problems else "合格") result = "合格" if not problems else " / ".join(problems) with LOG.open("a") as f: f.write(f"| {datetime.now():%Y-%m-%d} | {stamp} | {total_min:.1f}分 | {result} |\n") sys.exit(1 if problems else 0) if name == "main": main() 外部のライブラリは使っていません。必要なのは Python 3.9 以上と、aws、age、docker の3つのコマンドだけです。 うまく戻せると、こんな出力になります(数字はイメージです)。 ―― 訓練結果 ―― ダウンロード 41.2 秒 チェックサム確認 3.8 秒 復号 6.1 秒 MySQL起動 24.7 秒 インポート 188.4 秒 答え合わせ 12.9 秒 合計 4.6 分 判定: 合格 ここで見てほしいのは、合計よりも内訳です。データが増えると、インポートの時間はじわじわ伸びていきます。毎月の記録を drill_log.md に並べておくと、「このペースだと来年の春には RTO を超える」と、火事になる前に分かります。 自動の訓練でカバーできないこと このスクリプトが確かめているのは、「データベースとファイルが、壊れずに、新しい状態で戻ること」までです。アプリが実際に動くか、ログインできるか、画像が表示されるかまでは見ていません。 なので私は、毎月の自動訓練とは別に、年に2回は「全員で校庭まで」の訓練をしています。別の事業者の上にサーバーを1台立てて、持ち出し袋の中身だけでアプリを起動して、hosts ファイルで向き先を変えてブラウザで開く。ここまでやると、毎回なにかしら見つかります。前回は、PHP の拡張モジュールが1つ足りませんでした。手順書に書いていなかったからです。手順書は、その日のうちに直しました。 報知器:バックアップが「来なかった」ことに気づく 最後に、backup.sh の末尾の curl について。これは healthchecks.io のような死活監視サービスに「今夜も最後まで終わった」と伝えるためのものです。 バックアップの監視は、失敗したら通知するだけでは足りません。cron の設定が消えた、サーバーの時刻がずれた、そもそもスクリプトが動かなかった、という場合には、失敗の通知すら来ないからです。決まった時刻までに「終わった」という連絡が来なかったら鳴る、という報知器を一つ付けておくと、静かに止まっているバックアップに気づけます。 保護者の皆様へ(おたより) 小学校の避難訓練の日には、家にプリントが配られていました。それにならって、お客様向けに一つだけ書いておきます。 「うちのバックアップ、誰が、どこに取っているんだろう」と思ったら、まずは制作会社や担当者に、こう聞いてみてください。 「最後に、バックアップから戻してみたのはいつですか」 すぐに日付が返ってきたら安心です。少し間が空いたら、一度だけ訓練をしてみる価値があります。私も、そういう初回の訓練に立ち会うことがあります。一度やっておくと、二回目からはずっと楽になります。 校長先生のお話 最後は、校長先生のお話です。 校長先生のお話は長い、というのが小学生の共通認識でしたので、三つだけにします。 一つ。バックアップの数ではなく、戻した回数を数えましょう。 二つ。鍵と手順書は、燃える建物の外に置きましょう。 三つ。訓練でできないことは、本番でもできません。消防の世界でよく言われる言葉ですが、サーバーの世界でも、そのまま通じます。 喉元過ぎれば熱さを忘れる、と言います。IDCF のニュースも、1か月もすれば、きっとタイムラインから流れていきます。だからこそ、熱さを覚えているうちに、カレンダーに一つだけ予定を入れてください。「バックアップ避難訓練」。所要時間は、初回だけ半日ほど見ておけば十分です。 以上で、今日の訓練を終わります。各自、静かに教室へ戻ってください。 ……あ、ここは戻っていいです。訓練なので。

    1 like
  2. Yes, we have captive-browser deployed on laptops for exactly that reason.

    1 point
  3. Fixing WordPress cURL Errors WordPress uses a single class for all remote HTTP requests, WP_Http. This class uses cURL to handle these requests, and quite often I see developers encountering issues with localhost requests and self-signed certificates, especially if you’re using your own local certificate authority to sign your local development certificates. I use mkcert to do this myself, and it’s excellent. Problem One major problem, however, is that WordPress’s WP_Http class uses its own local certificate signing authority, one that’s included in the WordPress source code at /wp-includes/certificates/ca-bundle.crt. This bypasses whatever signing authority PHP has set itself, and is often the cause of the following cURL related PHP errors: cURL error 60: SSL certificate: unable to get local issuer certificate curl: (60) SSL certificate problem: unable to get local issuer certificate Solution To correct this, and repair any localhost cURL certificate issues for a WordPress site you can use the following function to bypass the local, hard coded CA used by WordPress for local TLD’s: /**  * Replace Localhost cURL Certificates  *  * If the URL for an HTTP request in WordPress contains a .test  * TLD, replace the WordPress local /certificates/ca-bundle.crt  * with the PHP supplied bundle.  */ function kevinlearynet_curl_patch_http_request_args( $parsed_args, $url ) { // Request URL points to a *.test host // Change this to your localhost domain tld $tld = end( explode( ".", parse_url( $url, PHP_URL_HOST ) ) ); if ( $tld !== 'test' ) return $parsed_args; // Replace WordPress local certificates with PHP's default cacert path $openssl_cert_locations = openssl_get_cert_locations(); if ( isset( $openssl_cert_locations['default_cert_file'] ) ) { $parsed_args['sslcertificates'] = $openssl_cert_locations['default_cert_file'] } return $parsed_args; } add_filter( 'http_request_args', 'kevinlearynet_curl_patch_http_request_args', 99, 2 ); Once you add this to your custom theme or functionality plugin, your cURL error 60 errors should disappear as WordPress uses the certificate authority provided by your PHP installation.

  4. Trusted SSL Certificates for Localhost The best way I’ve found to add SSL certificates to a localhost environment is using mkcert, which is technically not a self-signed certificate at all. Instead, it creates a local certificate signing authority, adds it to your machine, and uses that to sign certificates. The end result is a fully valid SSL certificate that is not self-signed, so it works with modern browsers and aligns with security protocols that block self-signed SSL certs. What is mkcert? mkcert is an open source command line tool written by Filippo Valsorda, a cryptographer who spent years on the Go security team at Google. As described on GitHub, it’s: > a simple tool for making locally-trusted development certificates that requires no configuration. Under the hood it does two things. First, it generates a private certificate authority (CA) that lives only on your machine and installs it into your system and browser trust stores, the same lists of trusted authorities that let your browser trust certificates from Let’s Encrypt or DigiCert. Second, it signs leaf certificates with that CA for whatever hostnames you ask for. Because the signing authority is trusted by your machine, every certificate it issues is trusted too: real HTTPS, green padlock, no warnings, no exceptions to click through. This is fundamentally different from the classic openssl self-signed certificate. A self-signed cert has no authority behind it, so browsers correctly treat it as untrustworthy. mkcert gives you the full chain of trust, just scoped privately to your computer. The private CA key never leaves your machine, which is also why this is safe: nobody else’s browser will ever trust certificates signed by your local CA. Fixing “Not Secure” Warnings on Localhost If you landed here trying to get rid of the “Your connection is not private” interstitial, the Not Secure label, or the red X through https:// in your address bar on a local site, this is the fix. Those warnings (usually NET::ERR_CERT_AUTHORITY_INVALID in Chrome or SEC_ERROR_UNKNOWN_ISSUER in Firefox) all mean the same thing: the certificate your local server presented isn’t signed by an authority your browser trusts. The common workarounds are all worse than the disease. Clicking “Proceed anyway” trains you to ignore security warnings and has to be repeated constantly. The chrome://flags/#allow-insecure-localhost flag was removed from Chrome entirely. And plain HTTP isn’t a real option anymore either, because features like service workers, secure cookies, HTTP/2, and many modern browser APIs only work in a secure context. mkcert gives your local environment full SSL, indistinguishable in the browser from a production site: valid certificate, padlock, no exceptions. Your dev environment behaves like production, which is the whole point of a dev environment. Installation mkcert supports Mac, Windows and Linux machines, so it should work with your setup. Mac OS & Linux brew install mkcert brew install nss The nss package is only needed if you use Firefox, which keeps its own certificate store separate from the operating system’s. Windows choco install mkcert Adding Localhost SSL Certificates Step 1: Create a Local Certificate Authority Once you’ve installed the tool, create the local certificate authority (CA) and install it into your trust stores: mkcert -install Step 2: Add SSL Certificates for Local URL’s Once you’ve set up a local authority for signing certificates, you can add SSL certificates for your localhost domains: mkcert example.test "*.example.test" localhost 127.0.0.1 ::1 This will create two *.pem certificate files, ./example.test+4.pem and ./example.test+4-key.pem, for the following hostnames: - example.test - *.example.test - localhost - 127.0.0.1 - ::1 These SSL certificates effectively cover the following URLs that are common localhost hosts: - https://example.test and any subdomain, like https://www.example.test - https://localhost Root Stores The mkcert -install step works by adding your local CA to every certificate root store it can find. It currently supports: - macOS system store - Windows system store - Linux variants that provide either update-ca-trust (Fedora, RHEL, CentOS), update-ca-certificates (Ubuntu, Debian, OpenSUSE, SLES), or trust (Arch) - Firefox (macOS and Linux only) - Chrome and Chromium - Java (when JAVA_HOME is set) To install the local root CA into only a subset of them, set the TRUST_STORES environment variable to a comma-separated list. The options are system, java, and nss (which includes Firefox): TRUST_STORES=system,nss mkcert -install Configuring the Local Web Server You’ll need to configure a host pointing example.test and www.example.test to your local web server. Once that’s set up, and you have the certificates added to your system, you _might_ be done. Many localhost servers automatically load certificates from your machine’s certificate store. If you restart your browser and the URL loads successfully with a secure, valid SSL certificate attached (lock icon to the left of your URL) then you’re all set. If you don’t see a secure connection yet, you may need to specifically point your local web server to use the *.pem files generated by mkcert in the previous steps. Apache Open your httpd.conf file and add or update the following to it: <VirtualHost *:443> ServerName example.test SSLEngine on SSLCertificateFile "/path/to/example.test+4.pem" SSLCertificateKeyFile "/path/to/example.test+4-key.pem" </VirtualHost> Restart Apache to load the certificates, then re-check your browser to verify they’re working. You may need to restart your browser to get your site(s) working. nginx Open your nginx.conf file and add or update the following to the http block: ssl_certificate "ssl/example.test+4.pem"; ssl_certificate_key "ssl/example.test+4-key.pem"; Restart nginx to load the certificates, then re-check your browser to verify they’re working. You may need to restart your browser to get your site(s) working. How I Use It for Local Development My setup is nginx configured to load the certificate files mkcert outputs, exactly as shown above. Every local project gets a .test domain, and they all share one certificate. When I spin up a new project and need another domain covered, I don’t touch the CA at all. I just re-run mkcert with the full list of hostnames: mkcert host.test host2.test host3.test …point nginx at the newly generated .pem files if the filenames changed, and restart nginx: sudo nginx -s reload That’s the entire workflow. The new certificate is signed by the same local CA my machine already trusts, so the browser accepts it immediately with no warnings and no browser restart. When do you need to re-run mkcert -install? Adding domains never requires it. mkcert -install only puts the CA into your trust stores, and once it’s there, every certificate the CA signs is trusted automatically. You only need to re-run it when the set of trust stores changes or the CA itself does: - A new machine, or a fresh OS install, where the CA has never been trusted - A new browser or root store appears: you install Firefox (after adding nss), or set JAVA_HOME for the first time, and that store needs the CA added to it - The CA was regenerated: if you delete the mkcert -CAROOT directory or sync dotfiles to a new machine without it, a new CA gets created and must be installed before its certificates are trusted If certificates that used to work suddenly throw authority warnings, re-running mkcert -install is the first thing to try; it’s idempotent and safe to run repeatedly. Related Links - mkcert on GitHub - Supported root stores (official README) - mkcert releases & pre-built binaries - Homebrew formula

  5. Afterlight: the ghost you leave in the machine, sealed on your own laptop This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine What I Built When someone dies, their family inherits a ghost: accounts nobody can find, subscriptions that keep billing, a laptop no one can unlock, thousands of photos on a drive no one knew existed. Almost nobody leaves a map. Afterlight is that map. It is a local-first digital estate planner. A language model running on your own laptop interviews you, one plain question at a time, and turns your answers into a letter of instruction for the people you love: where things are, who to call, what to cancel, what you want. The letter is encrypted, and the key is split into five printable cards. Any three unlock it. Nothing ever touches a server. I took "ghost in the machine" literally, and kindly. Día de los Muertos is about keeping the dead present through memory and care. Afterlight is the practical side of that: the last thing you do so that your family's grief is not also a scavenger hunt. It is not a chatbot that impersonates the dead. It never pretends to be anyone. What it does: - Interviews you. A local model asks natural follow-ups, but a state machine decides which topics are covered, so a small model stays on track. - Refuses secrets. If you paste a password, card number, or seed phrase, a guard blocks it before it reaches the model or the disk. The letter says where things are ("password manager, emergency kit in the blue folder"), never what they are. - Writes the letter from facts. Every fact comes from a validated record, not from model free text. The model only helps with personal messages, and you approve everything before sealing. - Seals with open standards. age encryption, with the key split by SLIP-39 into 5 cards, any 3 of which unlock it. - Recovers without me. The result opens with the stock age tool, so the family does not need Afterlight at all. I confirmed this: recovery opens with the stock age binary, no Afterlight code involved. Demo {% embed youtu.be %} Code {% embed hendrahend/afterlight %} How I Built It The central design choice is a hard split between what the model does and what plain code does. !Afterlight create flow !Afterlight recover flow Purple boxes are where the open-weight model works. Everything else is plain, testable code. The model: gemma3:4b (2.5 GB), served through Ollama on a Windows laptop with no GPU. The backend speaks an OpenAI-compatible API, and the model name lives in a config file, not in the code. I compared one model — gemma3:4b produced valid JSON on 5 of 5 answers at about 18 tokens/s, which was enough to move forward without a GPU. The interview: a state machine owns the checklist (accounts, money, devices, people, wishes). The model rephrases each question naturally and extracts answers into a JSON schema using constrained output. Pydantic validates the result. If the model fails, static fallback questions keep the interview going. A small model plus a strict scaffold turned out to be more reliable than a big model improvising. The guard: regex and entropy checks (Luhn for card numbers, seed-phrase patterns, high-entropy tokens) run on every answer before it goes anywhere. The seal: a random 128-bit secret becomes the age passphrase, and the same secret is split with SLIP-39 (shamir-mnemonic, Trezor's reference implementation) into 3-of-5 word shares with checksums, printed as cards with a QR code. A RECOVERY.txt in the package explains, in plain language, how to open everything with only age and any SLIP-39 tool. <!-- TODO: afterlight-recover-flow.png — upload and paste the URL --> _Recover flow diagram coming once I capture the screenshots._ Recovery needs no model at all, only open tools. Tests: 163 tests. Seal and recover round-trip for every 3-of-5 combination, fail correctly with only 2 shares, catch a single checksum typo in a share word, reject guard samples (API keys, card numbers, seed phrases), and a test fails on any non-loopback network connection. I wrote the spec first in AGENTS.md and built the code with opencode following it. I decided the architecture and the hard rules up front (local-first, no plaintext on disk, guard every answer, no non-loopback network); the agent implemented to that plan rather than inventing its own. Why Does Open Innovation Matter? Privacy is the precondition, not a feature. This is the most sensitive data a person owns: accounts, money, family, wishes. With a closed API, every answer in the interview would travel to someone else's server. With an open-weight model on my own laptop, nothing leaves. You do not have to take my word for it: the demo runs with Wi-Fi off, and the test suite fails if the code opens a non-local connection. This must outlive me. A digital legacy tool that depends on a company is a promise that expires when the company does. Afterlight's output uses open standards with independent implementations, age and SLIP-39. In thirty years, your family needs a copy of age and the instructions in RECOVERY.txt. They do not need this project, and they do not need me. The model is swappable. Changing the model is one line of config. That is what lets the same tool run on a modest laptop today and on whatever small model is best next year. It costs nothing to run, which matters for something people should do once and not pay a subscription to keep safe. The honest tradeoff: a small local model is not as capable as a frontier model, so it cannot be trusted to improvise. That pushed me to give it a narrow job inside a deterministic structure, and I think the tool is safer for it. The Spirit of It The first time I ran the interview on my own life, the questions were simple but the answers were not. "Where are your accounts?" — I could answer that. "Who should get the photo drive no one else knows about?" — I had to decide, and deciding meant admitting there was something to decide. The guard is the moment I respect most: I pasted a real-looking API key into the test on purpose, and it refused to seal. It would rather be a pain in the moment than a secret leaked in a letter. And when three cards opened the letter and two did not, the 3-of-5 math stopped being a theorem and became a promise I could actually keep for my family.

  6. @sahota_ad @asciidiego No it's really easy. There are scripts that even do both steps in one like mkcert filosottile/mkcert Browsers should already have eMudhra CA certs in their CRLs so I'd expect if you try it right now it would not work.

    1 like · 38 views
  7. YouTube · · YouTube description · The AI Toolbox · FiloSottile/age

    Are your passwords in Git in plain text? SOPS encrypts them SOPS (Secrets OPerationS) is an open-source command-line tool for encrypting secrets in config files: it encrypts the values in YAML, JSON, ENV and INI files (and whole binary files) with age, PGP, AWS KMS, GCP KMS, Azure Key Vault, HuaweiCloud KMS or HashiCorp Vault and OpenBao, so the secrets can sit in Git next to your config. The names stay readable and only the values are encrypted, so diffs stay useful. It's MPL 2.0 licensed and a CNCF Sandbox project. In this video: - What SOPS is and how it works (one data key per file, locked for each key you choose; sops edit decrypts into your editor and encrypts again when you exit) - 3 real uses: commit encrypted secrets to Git, one .sops.yaml rules file for dev and prod keys, and passing secrets to an app with exec-env - The catch: you manage the keys, losing every key means losing the data, names stay readable, and what to do if a key leaks - Who it's for, an alternative, and how to try it Three real uses: 1. Commit secrets to Git: each value is encrypted with AES-256-GCM (AES256_GCM), so a diff points to the value that changed (the sops: block's date and checksum change too). With a .gitattributes entry and git config diff.sopsdiffer.textconv "sops decrypt", git diff can show decrypted diffs to anyone who holds a key. 2. One rules file: a .sops.yaml at the repo root holds creation_rules, where path_regex picks the keys for each file, for example one age key for development files and another for production. The first matching rule wins. 3. Pass secrets to an app: sops exec-env puts the decrypted values in one child process's environment, never written to disk; sops exec-file hands the app a FIFO or a temporary file instead. Also in the docs: --encrypted-regex to encrypt only the data and stringData values of a Kubernetes secrets file, key groups with Shamir's Secret Sharing (for example, keys from 2 of 3 groups needed to decrypt), sops updatekeys and sops rotate, and an optional audit log to PostgreSQL. The catch: SOPS doesn't create or store your master keys. You bring age or PGP keys, a cloud key service (AWS KMS, Google Cloud KMS, Azure Key Vault and HuaweiCloud KMS are paid services of those cloud providers) or HashiCorp Vault or OpenBao. A file can be decrypted as long as one of its keys still works, so if you lose every one, the data can't be recovered: keep a backup key (the docs describe a spare PGP key kept securely for emergency decryption). Only values are encrypted: names and the file's structure stay readable, and the docs advise keeping encrypted files reasonably private. If a key leaks, remove it from .sops.yaml, run sops updatekeys, then sops rotate for a new data key, and only then change the passwords themselves. YAML anchors and top-level arrays aren't supported, and the key service connection has no authentication or encryption of its own (the docs suggest an SSH tunnel). There's no web interface or central secrets server: it's a command-line tool. Try it (with dummy values): Download a binary for your platform from github.com/getsops/sops/releases age-keygen -o key.txt (age-keygen comes with age, its own tool) sops encrypt --age "$(age-keygen -y key.txt)" --output test.enc.yaml test.yaml export SOPS_AGE_KEY_FILE=key.txt sops decrypt test.enc.yaml Back up key.txt: it's your private key. Links: Repo: getsops/sops Docs: getsops.io Installation: getsops.io age keys: getsops.io .sops.yaml rules: getsops.io Key management and rotation: getsops.io exec-env and exec-file: getsops.io Security and threat model: getsops.io Licence (MPL 2.0): github.com/getsops/sops/blob/main/LICENSE age: FiloSottile/age Alternative: Infisical, Infisical/infisical Facts checked 2026-10-20 (SOPS v3.13.3, released 2026-07-23; 23,455 GitHub stars). Started at Mozilla in 2015, a CNCF Sandbox project since 2023. An open-source option to consider, not an endorsement. Not affiliated with the SOPS project, the CNCF, Mozilla or Infisical. Narration: AI voice. #SOPS #SecretsManagement #DevOps Chapters 0:00 Intro 0:14 The fix 0:27 How it works 0:42 Three real uses 1:01 The catch 1:25 Who it's for 1:35 Try it

    16 views
  8. Claude Traced the NIST Curve Seeds to a 1997 Clock and lrand48. Nothing to Rotate on P-256 - Book: System Design Pocket Guide: Interviews - The series: Pocket Guides for Developers — 8 short books on agents, RAG, prompts, observability, events, databases and system design — the whole series - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- The seeds behind P-256, the curve under every ES256 signature on a JWT, were not the hash of a secret NSA sentence. According to Anthropic's write-up from October 16, they came out of the C library's lrand48 random-number generator, "seeded with a Unix timestamp", on dates between May 1997 and April 1998. Anthropic says an internal research model worked that out "in under half an hour." The cryptographer who ran a three-year-old bounty on the question, Filippo Valsorda, wrote the same day that the bounty had been claimed, and that he "reimplemented the algorithm in Go to verify the claim." Before you go looking for something to patch: Anthropic's own post says "There is no action to be taken or things to do differently." What the story does give you is a settled argument from the late 1990s, and a good reason to check what mints your session tokens. The 1990s riddle When NIST standardised ECDSA in FIPS 186-2 in 2000, the curves P-192 through P-521 were each derived from a seed. Anthropic's post says those seeds "were provided by the NSA with no explanation or justification." The best-known account was that they were a hash of a humorous passphrase, and nobody could find the passphrase. That gap fed suspicion. If a curve's maker can pick its seed freely, they can, in theory, keep trying seeds until a curve has a property only they know about. Anthropic notes people have long wondered whether the seeds "may have been chosen to have deliberate backdoors in them." Nobody had evidence of one, as BleepingComputer pointed out in 2023, but nobody could rule it out either. So in 2023 Valsorda and a group of cryptographers put up $12,288 for the original sentences, tripled to $36,864 if the winner gave it to charity. What Anthropic says Claude found The answer was not a sentence at all. Per Anthropic, "Claude guessed the software used a pseudorandom number generator (PRNG) that shipped with 1990s C libraries," then recovered that generator's internal state from the published seeds, which the post calls "a textbook lattice problem." The details from the post: - The P-192, P-224, P-256, P-384 and P-521 seeds are lrand48 output, each run seeded with a Unix timestamp. P-256's run decodes to August 1997. - Four binary-curve seeds (B-233, B-283, B-409, B-571) fit the same scheme with a clock that appears to have been halved. - "For P-256, the published seed is candidate 1842, but candidate 876 of the same run also gives a prime-order curve." Why 876 was skipped is unknown. - The base point for P-192 and five ANSI X9.62 example curves came from another generator, FreeLIP's, with small integer seeds. Anthropic names lrand48; Valsorda's post says drand48. Both belong to the same POSIX family, which generates numbers "using a linear congruential algorithm and 48-bit integer arithmetic." Same engine, different output format. !An old 1990s desk clock whose gears feed a ribbon of tiles into a smooth elliptic curve, drawn as an editorial illustration The seeds came from a clock and a 48-bit generator, not a hidden sentence. Anthropic published two Python scripts that reproduce the seeds and selected base points, dedicated to the public domain under CC0. The team claimed the bounty and chose to donate it to the Internet Security Research Group, the non-profit behind Let's Encrypt, which tripled it to $36,864, according to Valsorda. Why "no backdoor" is now the strong reading Valsorda's post makes the argument in numbers. A 2017 idea from Bernstein and Lange needed roughly 70 bits of freedom in choosing seeds to aim at a class of weak curves. With timestamps and a generator, Valsorda writes that even if the NSA could pick any timestamp in 1997–98 and any of the first 16 prime-order candidates, "they would have had barely 30 bits of freedom." He says the findings "completely rule out the technique described by Bernstein and Lange." Anthropic's phrasing is more careful: the findings make a backdoor "much less plausible." And the post is explicit about scope: "This is not a cryptographic break, new attack, or weakness in the ECDSA curves." Valsorda goes further. He says this "nicely completes the 'rehabilitation arc' of NIST elliptic curve cryptography" and, given modern deterministic signing and verified implementations, calls the NIST curves ones "which I would now personally recommend over Curve25519/Ed25519/X25519." What changes in your code For P-256 itself: nothing. Don't rotate keys, don't swap curves because of this. Anthropic says so in as many words. The useful lesson is the generator. A 1997 program seeded a non-cryptographic PRNG with the clock, and decades later its output could be traced back to the second it ran. POSIX says the drand48 family "should be avoided whenever non-trivial requirements (including safety) have to be fulfilled." Python's random docs and MDN's Math.random() page carry the same warning: not for security. If you generate session IDs, reset tokens, API keys or invite codes, find out what produces them. git grep gets you a list of places to read: git grep -nE \ -e 'srand(48)?\(' -e '[dlm]rand48' \ -e 'Math\.random' -e '"math/rand' \ -e '^[[:space:]]*(import random|from random import)' The last two patterns catch Go files importing math/rand and Python files importing random, even when nothing ever calls a seed function. A hit is not a bug. Shuffling a test fixture with Math.random() is fine. A hit that ends up in a token, a password or a URL someone shouldn't guess is the one to fix. !A die split open to reveal a ticking clock face inside, with a magnifying glass hovering over it, as a flat editorial illustration A PRNG seeded from the time is only as secret as the time it ran. The fix is the operating system's cryptographic generator, which every mainstream runtime exposes. In Python, the secrets module: import secrets token = secrets.token_urlsafe(32) In Node.js, crypto.randomBytes: import { randomBytes } from "node:crypto"; const token = randomBytes(32).toString("base64url"); In Go, crypto/rand, not math/rand: import ( "crypto/rand" "encoding/hex" ) func newToken() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil } In the browser, MDN points to crypto.getRandomValues(). What's still open, and what the AI part means The riddle isn't fully closed. Anthropic writes that "the fifth binary seed, B-163, fits nothing Claude has tried and remains unexplained," and that "we don't know how the base points for P-224, P-256, P-384, and P-521 were generated." Valsorda adds that of two extra selection criteria, one "still escapes us." On the AI side, be precise about what is public. Anthropic doesn't name the model beyond "an internal research model," and the half-hour figure is its own account of an internal run. What outsiders can check is the result, and that has been checked: the scripts are public, and Valsorda's Go reimplementation is on GitHub. A question that had stood since the late 1990s fell to a guess about old C libraries plus a standard lattice technique, and a human expert confirmed it in code within a day. I haven't run either script myself. If you have, or if you went looking through your own repo for Math.random() tokens after reading this, tell me what turned up in the comments. --- If this was useful I build Hermes IDE, a free, source-available IDE (BSL 1.1) for macOS, Windows and Linux that runs Claude Code, Codex and other coding agents side by side, each task on its own branch and worktree. If you want an agent to work through that git grep list without touching your main checkout, download it or read the source on GitHub. It isn't affiliated with Anthropic or any agent vendor. System design interviews keep coming back to the same small decisions: how you mint short links, IDs and session tokens, and what breaks if someone can predict them. The System Design Pocket Guide: Interviews walks through 15 real designs step by step, trade-offs included, so you can reason about those choices out loud. It's one of 8 short books in the Pocket Guides for Developers series, covering agents, RAG, prompts, observability, events, databases and system design. ![System Design Pocket Guide: Interviews — 15 Real System Designs, Step by Step](amazon.com)

  9. Cryptography Dispatches: The NIST Elliptic Curves Seeds Bounty Has Been Claimed The NIST Elliptic Curves Seeds Bounty Has Been Claimed We now know how the seeds of the NIST elliptic curves were generated: they’re not the hash of some saucy sentence, but the output of drand48 seeded with the time, which was indeed in 1997–98. John Wood, Henry de Valence, Ava Howell, Zack Voase, Lucy Sweet, Amir Omidi, Paul Crowley, Iain McGinniss, Nicholas Carlini, and Steve Weis from Anthropic solved the riddle and on October 15th, 2026, at 01:07:51 UTC, claimed the full bounty I announced three years ago. They chose to donate the prize to ISRG, the non-profit that operates Let’s Encrypt, tripling its value to $36,864 (36 Ki$). Thank you to Amir Omidi, Chris Palmer, Colm MacCárthaigh, David Adrian, David Anderson, Jeff Hodges, Matt Green, Matthew McPherrin, Paul Kehrer, Ryan Sleevi, and Soatok for contributing to the bounty. This helps fill out a page of cryptographic history, and reinforces our confidence in these critical cryptographic primitives. The NIST elliptic curves (P-192, P-224, P-256, P-384, and P-521) were published by NIST in FIPS 186-2 in 2000. They were generated at random according to ANSI X9.62 by taking an arbitrary seed, hashing it with SHA-1, and using the output to derive some of the parameters. Until this week, we had no idea how those arbitrary seeds were picked. The lore was that Dr. Jerry Solinas, who passed away in early 2023, generated them in 1997 by hashing some English sentences that he later forgot. You can read more in this article by Steve Weis or in the original bounty announcement. Turns out memory can play tricks on everyone: we now know they were instead generated with the POSIX-standard drand48 random number generator. The algorithm is as simple as passing the then-current UNIX timestamp to srand48() and then producing one byte at a time with drand48. Each sequence of 20 bytes is tried as a seed following ANSI X9.62 and rejected if the resulting curve is not valid. Some timestamps are used to generate multiple curves, by continuing to try seeds after accepting the first one. I reimplemented the algorithm in Go to verify the claim. Read more in Anthropic's writeup. There are two additional undocumented criteria beyond the ANSI X9.62 ones, one fairly clear and one that still escapes us. If the bit length of the order n of the resulting curve is more than the bit length of the base field p of the curve, the seed is rejected. (The condition is not the stricter n < p, because prime239v2 has p < n < 2²³⁹.) This leaves us with roughly twice as many valid seeds as were published. We explored a number of explanations for this last bit, and found no good fit. Maybe the same RNG was used to validate seeds, consuming RNG output and skipping some potential candidates, although the “gaps” are not regular. Maybe there was just a bug in one of the algorithms that returned the wrong result half the time; these calculations, which we can now do with off-the-shelf software in minutes, were still very much state-of-the-art in 1997. srand48(863711343) [1997-05-15 15:49:03 +0000 UTC]: accepted 36th seed 25e5[...]: not published accepted 1562nd seed d09b[...]: not published accepted 2185th seed e43b[...]: prime239v1 accepted 2320th seed e8b4[...]: prime239v2 accepted 2491st seed 1256[...]: not published accepted 2682nd seed d2c1[...]: not published accepted 2863rd seed 3a70[...]: not published accepted 3048th seed 7d73[...]: prime239v3 srand48(863966734) [1997-05-18 14:45:34 +0000 UTC]: accepted 139th seed 3045[...]: P-192 a.k.a. prime192v1 accepted 2198th seed 692d[...]: not published accepted 5228th seed c469[...]: prime192v3 accepted 5405th seed 3ad1[...]: not published accepted 6013th seed 31a9[...]: prime192v2 srand48(871002724) [1997-08-08 01:12:04 +0000 UTC]: accepted 1136th seed d09e[...]: P-521 srand48(871696354) [1997-08-16 01:52:34 +0000 UTC]: accepted 876th seed 50c7[...]: not published accepted 1842nd seed c49d[...]: P-256 a.k.a. prime256v1 srand48(872952608) [1997-08-30 14:50:08 +0000 UTC]: accepted 2688th seed a335[...]: P-384 srand48(891607927) [1998-04-03 12:52:07 +0000 UTC]: accepted 777th seed d007[...]: not published accepted 1485th seed 18cf[...]: not published accepted 5736th seed 3c17[...]: not published accepted 7026th seed bd71[...]: P-224 What does this mean for the security of elliptic curve cryptography based on the NIST curves? Well, even before this discovery there were no serious concerns among practitioners. In Section 3.1 of their 2015 paper “A riddle wrapped in an enigma,” Koblitz and Menezes make a convincing argument that it’s implausible that the NSA could have selected a seed to exploit a weakness that escaped more than 15 (now 25) years of cryptanalysis. However, as reported in a later version of that paper, Daniel Bernstein and Tanja Lange described to Koblitz and Menezes in 2017 a technique that could have exploited 70 bits of freedom in selection of the seed to target a hypothetical class of 2⁶¹ weak prime-order curves. (Bernstein is the author of a competing elliptic curve design, Curve25519.) This contributed to enduring conspiracy theories, which had significant appeal among the non-specialist public (although many have since moved on to other, adjacent, conspiracy theories). All this speculation harmed confidence in robust cryptographic systems and was part of my motivation for the bounty. This week’s findings completely rule out the technique described by Bernstein and Lange. Even if the NSA had been free to pick any timestamp in 1997–98 and any of the first 16 prime-order candidates (which generously accounts for the unexplained rejections), they would have had barely 30 bits of freedom. Not enough to intentionally target a hypothetical small class of weak curves, not enough to embed any kind of NOBUS backdoor. Along with deterministic and hedged ECDSA signing algorithms, formally verified field implementations, and the 2015 complete addition formulas by Renes, Costello, and Batina, this nicely completes the “rehabilitation arc” of NIST elliptic curve cryptography, which I would now personally recommend over Curve25519/Ed25519/X25519. Just in time for them to be retired in favor of post-quantum signatures. P.S. The Anthropic team also figured out how the seeds of the NIST B-curves (the ones over binary fields) were generated, but no one uses those, so who cares. The picture In a FamilyMart in Taiwan I found this delightful dice set. A full roll carries log₂(6²⁰) ≈ 51.7 bits of entropy. Rolling them twice can make a pretty robust cryptographic key! I thought it would be funny to label them as Geomys FIPS 140-3 Entropy Sources (which of course they are not, though we do have one of those) and hand them out as swag. However, I found no way to order them online, so I simply visited over a dozen FamilyMarts and brought home 50 packs. Speaking of swag, I am 100% making stickers with the srand48() seeds, and t-shirts for the Anthropic team and the contributors to the bounty. I accept suggestions for other seed-based swag. If you want to catch some, follow me on Bluesky at @filippo.abyssdomain.expert or on Mastodon at @filippo@abyssdomain.expert. Read more: Did you like this? There is no tracking, so reply to let me know! My snail mail address is 9450 SW Gemini Dr #52960, Beaverton, OR 97008 and I enjoy postcards 📮 You can narrow your topics to only cryptography or only open source maintenance. Do you want me to write more? You can sign up to pay $7/issue (max. 2 per month). My brain's reward function is easily tricked. reimplemented the algorithm in Go: github.com/FiloSottile/mostly-harmless/blob/main/… formally verified field implementations: mit-plv/fiat-crypto

  10. Firefox Certificate Handling: HTTPS Errors, Self-Signed Certs, and Corporate PKI SSL/TLS certificates are how HTTPS works — but Firefox's certificate handling has details that matter to developers and security-conscious users. This guide covers what Firefox checks, what errors mean, and how to work with certificates in development environments. What Firefox Checks When Loading HTTPS Sites Firefox validates several things for every HTTPS connection: 1. Certificate chain: the site's cert must chain up to a trusted root CA 2. Expiry: cert must not be expired 3. Hostname match: cert must match the domain being visited (Common Name or Subject Alternative Names) 4. Revocation: cert must not be on OCSP revocation lists (Firefox uses soft-fail by default) 5. Algorithm: SHA-1 certs are rejected; minimum 2048-bit RSA keys The Firefox Certificate Store Firefox maintains its own certificate store, independent of the OS. This is one area where Firefox differs fundamentally from Chrome/Edge on Windows, which use the Windows certificate store. Consequences: - Corporate-issued root CAs added via Windows Group Policy don't automatically appear in Firefox - Enterprise MITM proxies (for traffic inspection) must explicitly add their CA to Firefox - Security tools that inject a CA certificate via the OS won't affect Firefox To view Firefox's certificate store: Settings → Privacy & Security → Certificates → View Certificates Three tabs: - Authorities: trusted root CAs (Mozilla's root store + any you've added) - Servers: manually-added exceptions for specific sites - People: client certificates (rarely used by most people) Common Certificate Errors SEC_ERROR_EXPIRED_CERTIFICATE: The cert's notAfter date has passed. Most Let's Encrypt certs expire every 90 days — usually this means auto-renewal failed. SEC_ERROR_UNKNOWN_ISSUER: The cert's signing CA isn't in Firefox's trust store. Common in development (self-signed certs) and corporate proxies. SSL_ERROR_BAD_CERT_DOMAIN: The hostname doesn't match the cert. The cert might cover www.example.com but you navigated to example.com (or vice versa), or a CDN cert doesn't include your domain. MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED: Usually DigiCert or similar CA's extended validation issue. SEC_ERROR_OCSP_INVALID_SIGNING_CERT: OCSP stapling misconfiguration on the server. Working With Self-Signed Certs in Development For localhost or internal dev environments: Option 1: mkcert (recommended) mkcert is a tool that creates a local CA and installs it into your system and Firefox trust store: brew install mkcert mkcert -install # adds CA to Firefox, system, etc. mkcert localhost 127.0.0.1 ::1 # creates localhost+2.pem and localhost+2-key.pem This eliminates cert warnings for all future local dev without manual exceptions. Option 2: Add a temporary exception If you just need to proceed past the error once: on the error page click "Advanced" → "Accept the Risk and Continue". This adds a permanent server exception to Firefox's cert store (visible under View Certificates → Servers). Option 3: Use HTTP for local dev http://localhost doesn't require any certs. Firefox (unlike Chrome) doesn't force HTTPS for localhost. For local API servers, plain HTTP often works fine. Importing a Corporate Root CA If your company uses a MITM proxy or internal PKI: 1. Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import 2. Select the .crt or .pem file your IT team provided 3. Check "Trust this CA to identify websites" → OK Firefox also has a setting to use OS certificate stores: security.enterprise_roots.enabled in about:config. Setting this to true makes Firefox honor root CAs added via the OS (Windows, macOS, Linux NSS), which is useful in enterprise environments. Client Certificates For mutual TLS (mTLS) where the server requires the browser to present a certificate: Settings → Privacy & Security → Certificates → View Certificates → Your Certificates → Import Firefox will prompt you to select the appropriate client cert when a server requests it. Set "When a server requests my personal certificate" to "Select one automatically" if you only have one cert, or "Ask every time" if you have multiple. Checking a Site's Certificate Click the padlock icon (or the lock in the URL bar) → Connection Secure → More Information → Security tab → View Certificate. This shows: - Issued to / Issued by - Serial Number - Validity period - SHA-256 fingerprint - Subject Alternative Names (the domains the cert covers) - Signature algorithm For developers debugging cert issues, the Security tab in DevTools (F12) also shows certificate chain info when you select a network request. HSTS (HTTP Strict Transport Security) HSTS tells browsers "only connect to this domain over HTTPS, and reject any HTTP connections." Sites set this via the Strict-Transport-Security response header. Firefox caches HSTS settings. To clear a site's HSTS status: Settings → Privacy & Security → Cookies and Site Data → Clear Data, or access about:config and search for the domain's HSTS entry (there's no built-in UI — use about:config → search security.HSTS). Firefox also ships with a preloaded HSTS list (domains that have asked to be HSTS-preloaded). For localhost testing, Firefox respects localhost. (with trailing dot) as a signal to bypass HSTS. Certificate Transparency Since Chrome and Safari required it first, most CAs now log all certificates to public CT logs. Firefox doesn't enforce CT requirements (as of 2024), but the infrastructure means every cert issued by a major CA is publicly logged and auditable at crt.sh. If you want to see what certs have been issued for your domain: https://crt.sh/?q=yourdomain.com about:config Certificate Settings Relevant preferences in about:config: | Preference | Default | Notes | |------------|---------|-------| | security.OCSP.enabled | 1 | 0 = disable OCSP, 1 = soft-fail, 2 = hard-fail | | security.enterprise_roots.enabled | false | Import OS root CAs | | security.tls.version.min | 3 | TLS 1.2 minimum (3=TLS 1.2) | | security.ssl.require_safe_negotiation | false | Reject non-RFC-5746 handshakes | | security.cert_pinning.enforcement_level | 1 | Certificate pinning level | Most users should leave these at defaults. The only common adjustment is security.enterprise_roots.enabled for corporate environments.

Achievements

Awards earned by the repositories filosottile owns. Select an award to embed its badge.

Gold
101st place finishes
Silver
72nd place finishes
Bronze
83rd place finishes
Awarded repositories
3Owned by filosottile now

Repositories

Ordered by gold, then silver, then bronze.

  1. mkcertBest: 1st by stars, 3,886 stars833
  2. HeartbleedBest: 1st by stars, 662 stars134
  3. ageBest: 1st by stars, 927 stars111

Every award

Top-three finishes of these repositories in completed trending days and weeks, newest first.

  1. age
  2. age
  3. age
  4. mkcert
  5. mkcert
  6. mkcert
  7. mkcert
  8. mkcert

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.