ActivityPub 구현이 어려운 이유, 그리고 그럴 필요가 없는 이유

2 hours ago 2
  • ActivityPub 서버를 직접 만들면 첫 Follow 요청부터 설명 없는 401 Unauthorized에 막히기 쉽고, Fedify는 서명·JSON-LD·전달·보안 부담을 애플리케이션 코드 밖으로 옮기는 TypeScript 프레임워크임
  • 페디버스 인증은 만료 초안 draft-cavage-http-signatures-12와 표준 RFC 9421이 함께 쓰이며, 문서 서명까지 포함하면 네 가지 서명 메커니즘과 RSA·Ed25519 키를 다뤄야 함
  • 같은 ActivityPub 활동도 JSON-LD에서는 문자열, 배열, 인라인 객체, URI 참조 등 여러 형태로 도착해, 직접 구현할수록 방어 코드가 코드베이스 전체에 퍼짐
  • 분산 전달에서는 Delete가 Create보다 먼저 도착하는 “좀비 포스트” 같은 문제가 생기며, 큐·재시도·멱등성·순서 보장·회로 차단기가 필요함
  • 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 Error와 410 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만 해도 구현체별로 세 가지 속성 이름이 쓰임
  • 상호운용은 한 번 맞추고 끝나는 작업이 아니라 계속 유지해야 하는 영역임

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

  • 들어오는 활동의 서명 검증을 건너뛰면 누구나 위조된 Follow나 Delete를 주입할 수 있음
  • 문서 로더를 제한하지 않으면 악의적 활동이 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-044f의 quote와 함께 하나의 API 뒤로 통합됨
      • Misskey의 isCat 속성도 타입으로 존재해 타입 안전하게 처리할 수 있음
  • 전달 인프라와 순서 보장

    • createFederation()에 메시지 큐를 연결하면 전달이 백그라운드로 이동하고, 실패 시 기본 최대 10회까지 지수 백오프로 자동 재시도함
    • 글 하나가 수천 팔로워에게 전달될 때는 two-stage fan-out이 동작함
      • 하나의 통합 메시지가 큐에 들어감
      • 백그라운드 워커가 서버별 전달 작업으로 분할함
      • 게시 버튼은 즉시 응답함
    • 재시도로 같은 활동이 두 번 도착할 수 있어, Fedify는 처리된 활동을 24시간 보관하는 멱등성 캐시로 중복을 핸들러 전에 건너뜀
    • sendActivity() 호출에 { orderingKey: post.id }를 지정하면 같은 orderingKey를 공유하는 활동은 각 수신 서버에 보낸 순서대로 전달됨
      • Delete가 Create를 앞지를 수 없음
      • 키가 다른 활동은 병렬로 나가 처리량을 유지함
    • 404 Not Found나 410 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를 이용할 수 있음
Read Entire Article