1P by GN⁺ | ★ favorite | 댓글 1개
  • ActivityPub 서버를 직접 만들면 첫 Follow 요청부터 설명 없는 401 Unauthorized에 막히기 쉽고, Fedify는 서명·JSON-LD·전달·보안 부담을 애플리케이션 코드 밖으로 옮기는 TypeScript 프레임워크임
  • 페디버스 인증은 만료 초안 draft-cavage-http-signatures-12와 표준 RFC 9421이 함께 쓰이며, 문서 서명까지 포함하면 네 가지 서명 메커니즘과 RSA·Ed25519 키를 다뤄야 함
  • 같은 ActivityPub 활동도 JSON-LD에서는 문자열, 배열, 인라인 객체, URI 참조 등 여러 형태로 도착해, 직접 구현할수록 방어 코드가 코드베이스 전체에 퍼짐
  • 분산 전달에서는 DeleteCreate보다 먼저 도착하는 “좀비 포스트” 같은 문제가 생기며, 큐·재시도·멱등성·순서 보장·회로 차단기가 필요함
  • Fedify는 13개 웹 프레임워크 통합, KV·메시지 큐 어댑터, CLI·린터·디버거·OpenTelemetry를 제공해 ActivityPub 세부 지식 없이 연합 앱 개발을 시작할 수 있게 함

직접 ActivityPub을 구현할 때 부딪히는 문제

  • Follow 활동을 Mastodon에 보내려면 JSON 작성, HTTP 요청 서명, POST까지 처리해야 하지만 실패하면 401 Unauthorized 한 줄만 돌아올 수 있음
    • 원인은 Date 헤더의 시계 오차, Digest 해시 오류, (request-target) 대소문자, 공개키 표현 방식 등일 수 있음
    • 원격 서버가 이유를 알려주지 않으면 다른 서버 코드를 읽으며 디버깅해야 함
  • Fedify는 단일 사용자 마이크로블로그 서버 Hollo를 만들던 과정에서 출발함
    • ActivityPub 구현 부담이 제품 개발을 삼키자, 앱보다 먼저 필요한 프레임워크로 만들어짐
  • 어려움은 크게 서명 표준, JSON-LD 문서 형태, 분산 전달, 구현체별 관행, 기본 보안 설정에 집중됨

서명 표준이 하나가 아님

  • 서버 간 인증에는 HTTP 서명을 쓰지만, 실제 페디버스에서는 만료된 초안 draft-cavage-http-signatures-12와 표준 RFC 9421이 함께 존재함
  • 어떤 서버가 어느 서명을 받는지는 시도 전까지 알 수 없어, 한 방식으로 서명한 뒤 거절되면 다른 방식으로 다시 서명하고 성공한 방식을 서버별로 기억해야 함
  • HTTP 서명은 요청 발신자만 증명하므로, 받은 활동을 제3자에게 전달하는 inbox forwarding 같은 상황에서는 문서 자체에 붙는 서명도 필요함

JSON-LD 문서 형태가 계속 달라짐

  • ActivityPub의 전송 형식은 JSON-LD이며, 같은 의미의 Create 활동도 여러 모양으로 표현될 수 있음
    • actor는 URI 문자열일 수도 있고 인라인 Person 객체일 수도 있음
    • to는 문자열 하나일 수도, 배열일 수도 있음
    • object는 인라인 객체나 URI 모두 가능함
  • 공개 대상을 뜻하는 주소도 https://www.w3.org/ns/activitystreams#Public, as:Public, Public 세 가지 표현이 모두 유효함
  • 명세에 맞게 처리하려면 JSON-LD 프로세서로 확장(expansion) 후 압축(compaction)해 정규화해야 함
    • 많은 구현은 이를 “그냥 JSON”처럼 다루다가 특정 서버가 내보낸 형태에서 조용히 깨짐
  • 직접 구현하면 값이 문자열인지, 배열인지, 객체인지, 가져와야 하는 URI인지 확인하는 방어 코드가 곳곳에 생김

분산 전달과 “좀비 포스트”

  • 사용자가 글을 올린 직후 오타를 보고 삭제하면 서버는 Create 다음 Delete를 보내지만, 네트워크 상황에 따라 수신 서버는 Delete를 먼저 받을 수 있음
    • 아직 없는 글의 삭제를 무시한 뒤 나중에 Create를 처리하면, 작성자는 삭제됐다고 믿는 글이 그 서버에 계속 남음
  • 팔로워가 5천 명이면 글 하나가 수천 건의 HTTP 전달을 만들며, 요청 핸들러 안에서 처리하면 게시 버튼 응답이 늦어지거나 서버가 무너질 수 있음
  • 큐를 쓰더라도 실패 전달의 재시도 일정, 지수 백오프, 재시도 횟수, 500 Internal Server Error410 Gone의 차이, 사라진 서버의 팔로워 정리, 장기 장애 호스트 처리까지 정해야 함
  • 이 영역은 단순 프로토콜 구현보다 분산 시스템 엔지니어링에 가까움

명세만으로는 상호운용이 끝나지 않음

  • 명세를 완벽히 지켜도 실제 페디버스 구현체와의 상호운용 문제는 남음
  • Mastodon의 secure mode는 GET 요청에도 HTTP 서명을 요구하는 authorized fetch를 사용함
    • 양쪽 서버가 모두 secure mode이면, 상대 공개키를 가져오려면 서명해야 하고 서명을 검증하려면 상대가 먼저 내 공개키를 가져와야 하는 교착 상태가 생김
    • 커뮤니티는 서버 자체를 나타내는 instance actor로 서명해 우회하지만, 이는 명세에 없음
  • Threads는 actor가 인라인 객체로 들어간 활동을 파싱하지 못해, Threads로 보낼 때는 actor를 URI로 보내야 함
  • Lemmy는 Mastodon이 요구하지 않는 Group actor 필드가 없으면 조용히 거절함
    • 예시는 attributedTo로 연결된 moderators collection과 featured collection임
  • Misskey는 자체 어휘 확장을 갖고 있으며, quote post만 해도 구현체별로 세 가지 속성 이름이 쓰임
  • 상호운용은 한 번 맞추고 끝나는 작업이 아니라 계속 유지해야 하는 영역임

직접 구현의 기본 상태는 안전하지 않음

  • 들어오는 활동의 서명 검증을 건너뛰면 누구나 위조된 FollowDelete를 주입할 수 있음
  • 문서 로더를 제한하지 않으면 악의적 활동이 http://169.254.169.254/나 내부 네트워크를 가리켜 서버를 SSRF 프록시로 만들 수 있음
  • 임베디드 객체의 출처 검사를 생략하면 어떤 서버든 특정 인물이 말한 것처럼 보이는 문서를 내보낼 수 있음
  • 이런 함정은 즉시 눈에 띄지 않고, 악용되기 전까지는 모든 것이 작동하는 것처럼 보일 수 있음

Fedify가 대신 처리하는 영역

  • FedifyActivityPub과 관련 표준으로 연합 서버 앱을 만들기 위한 TypeScript 라이브러리임
  • Deno, Node.js, Bun에서 실행되며 Cloudflare Workers 같은 edge 런타임도 지원함
  • 설계 목표는 서명, JSON-LD, 전달, 구현체별 차이, 보안 세부사항을 애플리케이션 코드에서 제거하는 것임
  • 서명 처리

    • actor dispatcher와 key pair dispatcher를 등록하면 actor 하나를 페디버스에 올릴 수 있음
    • 나가는 모든 요청에는 서명이 붙음
    • RSA 키에서는 HTTP Signatures와 Linked Data Signatures를 냄
    • Ed25519 키를 추가하면 Object Integrity Proofs도 붙음
    • 네 가지 메커니즘이 하나의 활동에 공존하고, 수신자는 자신이 이해하는 가장 강한 방식으로 검증함
    • Fedify는 double-knocking을 직접 처리함
      • 첫 접촉은 RFC 9421로 나가고, 거절되면 draft-cavage로 재시도함
      • 성공한 방식은 서버별로 캐시됨
      • 거절 응답에 Accept-Signature challenge가 있으면 서버가 요청한 구성요소로 다시 서명함
    • 들어오는 서명은 애플리케이션 코드가 보기 전에 검증되며, 검증 실패 활동은 리스너에 도달하지 않음
    • actor dispatcher 등록만으로 WebFinger RFC 7033 서버도 생겨, Mastodon 검색창에서 @alice@example.com 형태로 actor를 찾을 수 있음
  • JSON-LD 대신 타입을 다룸

    • Fedify는 Activity Vocabulary 전체와 주요 벤더 확장을 포괄하는 약 80개 클래스를 제공함
    • 클래스는 타입이 있고 불변이며, 접근자는 JSON-LD가 허용하는 문서 형태 차이를 흡수함
    • lookupObject()는 핸들을 받아 WebFinger discovery까지 포함한 전체 조회 절차를 실행함
    • getFollowers() 같은 접근자는 값이 URI 참조든 인라인 객체든 같은 방식으로 동작하고, 가져온 값은 캐시됨
    • 벤더별 차이도 API 뒤로 감춰짐
      • quoteUri, _misskey_quote, quoteUrl 세 가지 quote 속성은 새로 등장하는 FEP-044fquote와 함께 하나의 API 뒤로 통합됨
      • Misskey의 isCat 속성도 타입으로 존재해 타입 안전하게 처리할 수 있음
  • 전달 인프라와 순서 보장

    • createFederation()에 메시지 큐를 연결하면 전달이 백그라운드로 이동하고, 실패 시 기본 최대 10회까지 지수 백오프로 자동 재시도함
    • 글 하나가 수천 팔로워에게 전달될 때는 two-stage fan-out이 동작함
      • 하나의 통합 메시지가 큐에 들어감
      • 백그라운드 워커가 서버별 전달 작업으로 분할함
      • 게시 버튼은 즉시 응답함
    • 재시도로 같은 활동이 두 번 도착할 수 있어, Fedify는 처리된 활동을 24시간 보관하는 멱등성 캐시로 중복을 핸들러 전에 건너뜀
    • sendActivity() 호출에 { orderingKey: post.id }를 지정하면 같은 orderingKey를 공유하는 활동은 각 수신 서버에 보낸 순서대로 전달됨
      • DeleteCreate를 앞지를 수 없음
      • 키가 다른 활동은 병렬로 나가 처리량을 유지함
    • 404 Not Found410 Gone에서는 재시도를 멈추고 등록된 영구 전달 실패 핸들러를 호출함
    • shared inbox로 보낸 경우 그 뒤에 있는 팔로워 목록도 받아 사라진 계정을 정리할 수 있음
    • 반복 실패하는 호스트는 기본 활성화된 회로 차단기가 전달을 보류하고 주기적으로 복구를 확인함

구현체별 관행과 보안 기본값

  • Fedify는 authorized fetch에서 .authorize()를 dispatcher에 연결해 검증된 요청자 신원을 콜백으로 넘김
    • 차단 목록, 비공개 컬렉션 같은 처리는 애플리케이션 로직으로 작성할 수 있음
    • instance actor 교착 문제에도 지원 패턴이 있음
  • Threads의 인라인 actor 문제는 기본 활성화된 activity transformer가 나가는 활동의 인라인 actor를 URI로 바꿔 처리함
  • Lemmy가 요구하는 moderators collection은 custom collection API로 몇 줄에 노출할 수 있고, Lemmy의 JSON-LD context는 미리 포함됨
  • 새 상호운용 문제가 발견되면 수정은 각 애플리케이션이 아니라 Fedify에 들어감
  • 보안 기본값은 안전한 쪽을 향함
    • 서명 검증은 켜야 하는 기능이 아니라 테스트용으로 끄는 기능임
    • 문서 로더는 private address range와 loopback을 기본 차단하고 DNS rebinding도 고려함
    • SSRF에 노출되려면 테스트용임을 드러내는 이름의 옵션을 명시적으로 켜야 함
    • 임베디드 객체의 출처가 부모 문서와 다르면 접근자가 신뢰하지 않고 원본에서 다시 가져옴
    • 이 출처 기반 보안 모델은 FEP-fe34에 기반함

기존 스택과 개발 도구

  • Fedify는 기존 웹 스택에 맞도록 설계됐으며 13개 웹 프레임워크 통합을 제공함
    • Express, Hono, Fastify, Koa, NestJS, Elysia
    • Next.js, Nuxt, SvelteKit, Astro, SolidStart, Fresh
  • 미들웨어가 콘텐츠 협상을 처리해, 같은 URL이 브라우저에는 HTML을, 페디버스에는 JSON-LD를 제공할 수 있음
  • Fedify 자체 저장소에는 하나의 키-값 인터페이스만 요구함
    • Redis, PostgreSQL, MySQL/MariaDB, SQLite, Deno KV, Cloudflare Workers KV, in-memory 어댑터가 있음
  • 메시지 큐는 PostgreSQL, Redis, AMQP/RabbitMQ 등을 포함한 8가지가 제공되며, 맞는 것이 없으면 인터페이스를 직접 구현할 수 있음
  • 도메인 데이터는 기존 데이터베이스와 ORM에 그대로 둘 수 있음
  • 다른 라이브러리로 이미 federation을 운영 중이면 마이그레이션 가이드와 데이터 마이그레이션 스크립트로 activitypub-express 등에서 기존 팔로워를 잃지 않고 옮길 수 있음
  • 상위 패키지도 제공됨
    • @fedify/relay는 한 함수 호출로 완전한 ActivityPub relay 서버를 제공함
    • @fedify/backfill은 페디버스를 따라가며 불완전한 대화 스레드를 복원함
  • 개발 루프를 위한 도구

    • fedify init은 한 줄로 프로젝트를 스캐폴딩함
    • fedify tunnel은 로컬 서버를 HTTPS로 노출해 실제 Mastodon과 테스트할 수 있게 함
    • fedify inbox는 서버가 보내는 활동을 받을 임시 inbox 서버를 띄움
    • fedify lookup은 다른 서버가 게시한 객체를 검사할 수 있게 함
    • fedify lookup --authorized-fetch는 일회용 키 쌍을 만들고 임시 ActivityPub 서버를 세워 secure mode 뒤의 객체에 서명된 요청을 보냄
    • @fedify/lint는 actor에 inbox가 없는 경우 같은 20가지 상호운용 버그를 잡는 ActivityPub 전용 린터임
    • @fedify/testing의 mock으로 네트워크 없이 테스트를 실행할 수 있음
    • @fedify/debugger는 한 줄로 디버그 대시보드를 붙여 브라우저에서 활동과 서명 검증 결과를 실시간으로 볼 수 있게 함
    • 운영 환경에는 OpenTelemetry 계측이 내장돼 있으며 28개 span type과 37개 metric을 제공함
    • 모니터링 가이드와 ActivityPub용 부하 테스트 도구 fedify bench도 제공됨
    • 공식 문서는 30장 매뉴얼과 5개 튜토리얼로 구성되며, 큐 backlog를 보기 위한 PromQL 쿼리와 알림 규칙, Mastodon에서 아바타가 보이게 하는 속성 같은 실제 관행을 다룸

이미 쓰이는 사례와 시작 방법

  • Fedify는 실제 서비스에서 사용 중임
    • Ghost의 ActivityPub 서비스
    • ORCID 연구자 기록을 페디버스로 연결하는 Encyclia
    • Cloudflare Workers에서 serverless로 동작하는 SiliconBeest
    • 한국 블로깅 플랫폼 Typo Blue
    • 단일 사용자 마이크로블로깅 플랫폼 Hollo
    • 커뮤니티가 운영하는 Hackers' Pub
  • 튜토리얼은 규모별 예제를 제공함
  • Fedify의 목표는 더 많은 ActivityPub 전문가를 만드는 것이 아니라, 개발자가 ActivityPub의 세부사항을 몰라도 연합 앱을 만들 수 있게 하는 것임
  • 시작 명령은 npm init @fedify
  • 도움이 필요하면 Matrix room이나 GitHub Discussions를 이용할 수 있음

댓글과 토론

Lobste.rs 의견들
  • ActivityPub 프로젝트에 서로의 포크가 많은 이유가 이거임: 직접 전부 구현하는 것보다 남의 접근법을 파악하는 편이 더 쉬움
    글쓴이가 제안하는 것도 실제로 흔히 보이는 Misskey나 Pleroma 포크와 크게 다르지 않아 보임. 라이브러리에도 나름의 관점과 접근법이 있고 제어권을 많이 주지는 않는 듯함. 그래도 전체 서버를 포크할 때처럼 UI까지 강제하지는 않는다는 장점은 있음
    AP를 구현 중인 입장에서 가장 어려운 부분은 JSON-LD를 제대로 쓰는 좋은 방법이 없다는 것임. 객체를 표준 표현으로 쉽게 변환할 수 있다면 상호작용은 자연스럽게 따라올 텐데, 진짜 연결 문서처럼 쓰기에는 너무 비효율적이고, 원시 JSON 문서처럼 쓰면 수많은 예외 케이스에 죽어나감. 지금까지는 두 번째 접근을 택했다가 죽었음

    • 특히 서명을 생각하면 “객체의 표준 표현” 문제는 더 중요함. 예전 XML 정규화는 바로 이 서명 문제, 즉 수신자의 바이트 직렬화가 송신자의 것과 일치하도록 보장하기 위해 있었음
      JSON-LD 세계와 완전히 같은 문제는 아니지만, 완전히 무관하지도 않음
      다만 JSON 인접 기술 상당수가 비슷한 문제를 겪는다고 봄. 같은 논리적 스키마를 표현하는 JSON Schema 방식이 너무 많고, 그 때문에 JSON Schema 주변 기술과 상호작용하는 일이 우스울 정도로 끔찍해짐. 특히 OpenAPI 스키마는 비슷하지만 같지는 않은 공포물이고, 스키마 초안 버전 수까지 고려하지 않아도 이미 충분히 나쁨
    • AP 서버 구현을 생각해 왔지만 아직 시작하지는 않았으니 크게 걸러 들어야 함. 도움이 될 수 있는 한 가지는 애플리케이션을 더 작은 서비스들로 나누고, 액터 모델에 더 기대어 이를 “통합된” 인터페이스처럼 보이게 하는 방식임. 예를 들어 이메일 서버의 MTA와 MUA 분리를 배울 수 있음
      AP의 “MTA” 서비스는 발신함에서 메시지를 보내고 수신함으로 메시지를 받는 일을 맡음. JSON-LD 문서는 이 서비스 입장에서는 거의 덩어리 데이터에 가까움. 송신자와 수신자를 알아내기 위한 약간의 파싱은 필요하지만 그 이상은 많지 않음. 저장소도 파일 기반일 수 있고, 기억이 맞다면 go-ap가 그런 식을 씀
      AP의 “MUA”는 실제 애플리케이션임. JSON-LD 의미를 이해해야 하는 쪽임. PostgreSQL 같은 것을 써서 문서를 jsonb로 저장하고, 생성 컬럼과 뷰로 SQL 친화적인 형태를 제공할 수 있을 듯함. 그러면 객체 타입에 따라 문서를 가장 적절하게 표현하는 방식을 정할 수 있음
      또 다른 예로 검색 서비스도 액터로 모델링해서 결과를 임시 발신함으로 반환하게 만들 수 있음
  • 여러 구현체의 특이한 동작과 완화책을 정리한 매우 귀중한 목록임
    아쉽게도 GoActivityPub에는 그중 절반도 아직 구현하지 못했음

  • 글이 처음에는 기술적인 내용으로 시작해서 고마웠는데, 중간부터 자기 프레임워크 홍보로 방향을 튼 듯해서 읽는 맛이 떨어졌음
    TypeScript를 쓰는 일부 세계에서는 이런 구현상 특이점을 다시 발견하지 않아도 될 수 있어 다행임. 하지만 머릿속 모델로는 “이 조건과 상황에서는 이런 결과가 나오고, 이런 수정이 필요하다”는 기록이 있다면 TypeScript가 아닌 상황, 예컨대 형제 프로젝트인 GoActivityPub 작성자도 그 고생의 결과를 얻을 수 있음. 여기서는 그런 것들을 몇 가지 다루긴 했지만, 글은 특정 시점의 스냅샷이고 프로젝트는 시간이 지나며 모든 상호운용성 버그를 축적하려는 것처럼 보임
    현재 대안은 내가 보기엔 사람이 쓴 것이 아닌 커밋 메시지를 전부 읽어가며, Fedify 자체 버그와 상호운용성 버그를 구분하는 것뿐임
    특히 저장소가 AI에 “올인”한 것처럼 보이는데도 그런 장부 정리를 하지 않는 건 아이러니함. LLM에 대해 들어온 홍보는 반복 잡무를 자동화한다는 것이었음. 그렇다면 Claude가 GitHub 이슈를 만들거나, 더 좋게는 저장소 안의 .md 파일로 관찰된 결과와 Fedify가 이를 어떻게 고치는지 문서화하게 하면 됨. 자체 디버거도 있고 무엇을 뜻하는지 모를 “모범 사례”도 있으니 딱 맞는 일일 것임

    • 정말로 사소한 문제를 과장해서 ActivityPub의 실패처럼 제시함. 예를 들어 팔로워 5천 명이면 게시물 하나가 수천 건의 HTTP 전달이 되고, 이를 요청 처리기 안에서 직접 하면 게시 버튼 응답에 30초가 걸리거나 서버가 쓰러지니 큐를 쓰라는 식임
      왜 제3자 서비스로 보내는 요청을 인라인으로 수행함? 이런 건 웹 애플리케이션 기본기임. 제3자 서비스와 통신해야 하면 백그라운드 작업으로 보내야 함. 요청에 응답하는 데 필요 없는 정보라면 백그라운드 작업으로 보내야 함. 요청 처리기 안에서 이런 요청을 하다가 생기는 문제는 갈퀴를 밟아 얼굴을 맞는 수준의 자초한 문제지, ActivityPub과는 상관없음
      전달이 실패하면 재시도해야 하고, 일정은 어떻게 할지, 지수 백오프를 쓸지, 몇 번 할지, 500 Internal Server Error와 410 Gone을 같은 실패로 볼지 같은 것도 그냥 일반적인 웹 애플리케이션 개발 문제임. 작업 큐에서 제3자 서비스에 요청할 때 생기는 문제고 ActivityPub과 무관함. 대부분의 웹 프레임워크에는 합리적인 기본값이 있음. 어떤 오류가 났는지에 따라 재시도 여부를 정해야 하는 지점에서만 판단이 필요함. 410을 재시도하는 건 낭비지만 급히 해결해야 할 문제는 아님. 작업 큐의 메모리 압박은 늘리겠지만 몇 시간 안에 애플리케이션을 쓰러뜨릴 가능성은 낮음
  • “거절되는지 보고, 다른 방식으로 다시 서명하고, 서버별로 어떤 방식이 통했는지 기억하라”니 대체 뭘 읽고 있는 건가 싶음. 이래서 Mastodon 개발이 느린 건가?
    “게시물 하나가 수천 건의 HTTP 전달”이라니, 네트워크 시스템 프로그래밍과 큐잉에 뛰어난 언어로 유명한 Ruby에서 말이지
    믿기 어렵고, 이걸 라이브러리로 감싸둔 건 좋지만 그래도 좀 그렇다

  • Java로 ActivityPub을 구현해 본 뒤, 이런 서버 간 프로토콜은 그냥 git 위에 만드는 편이 낫다는 결론에 도달했음
    복잡성의 상당 부분이 git이 이미 더 잘 해결하는 문제를 다시 풀기 위해 존재함. 이를 git 저장소 안의 JSON 문서로 모델링하면 페이지네이션을 다루지 않아도 됨. 프로토콜이 이미 없는 데이터만 보내도록 보장하고, 커밋 서명도 얻고, 이벤트 순서 보장도 얻고, 이 글에서 언급한 문제도 해결되며, 이력도 공짜로 생김. 이런 프로토콜에는 버그 많고 느린 git 절반 구현이 들어 있다는 식의 Greenspun의 열 번째 법칙 비슷한 말을 만들 수 있을 듯함

    • git은 부모 커밋에 이력이 의존하므로 훌륭한 선택은 아님. 다만 비슷한 협상 전략으로 동작하는 머클 트리 가십 프로토콜은 잘 맞을 수 있음
  • 이 글은 AI가 만든 저품질 글처럼 읽힘
    더 구체적으로는 왜 이야기 형식으로 썼는지 모르겠음. 여기서 전달하는 사실들은 훨씬 더 간결하고 덜 편향적으로 쓸 수 있었고, 서사도 설득력이 없음. 특히 AI 특유의 표현들이 많아서 그렇다
    즐겁게 읽히지 않았음. 그래도 문제들을 짚어준 건 고맙고, 다른 방식으로 그 문제들을 읽고 고칠 수 있기를 바람