2P by GN⁺ | ★ favorite | 댓글 1개
  • BlogTech는 소규모에서 중간 규모 웹사이트를 관리하기 위한 C 기반 툴킷으로, HTTPS, 가상 호스트, ACME 기반 자동 인증서 관리, 원격 서버 관리 클라이언트를 제공함
  • 현재 테스트 단계이며, 버전 0.4.x로 ` 몇 주 동안 운영하는 데 사용됐지만 안정 릴리스로 제시되지는 않음
  • 서버는 Linux와 Windows에서 실행되지만 HTTPS는 Linux만 지원하며, Windows에서는 클라이언트와 서버 모두 HTTP만 사용 가능함
  • 원격 파일 관리는 --put, --get, --delete로 처리하고, 리소스를 변경하는 요청은 공유 비밀키 기반 HMAC/SHA256 서명으로 인증함
  • 운영 환경에서는 포트 80의 HTTP 서버와 도메인 DNS 레코드가 필요하며, ACME를 켜면 인증서와 개인키 파일을 자동 생성하고 오류는 acme.log에 남김

BlogTech의 목적과 현재 상태

  • BlogTech는 소규모에서 중간 규모 웹사이트 관리를 위한 툴킷임
  • 지원 기능은 다음과 같음
    • HTTPS

    • 가상 호스트

      • ACME를 통한 자동 인증서 관리
      • 서버 원격 관리를 위한 클라이언트
      • 아직 테스트 단계
      • 버전 0.4.xhttps://coz.is/를 몇 주 동안 성공적으로 서비스한 사례가 있음
      • 예전 단일 파일 버전은 single_file 브랜치에 있음

빌드와 기본 실행

  • BlogTech는 Linux와 Windows에서 실행됨
    • Linux 빌드에는 OpenSSL 개발 라이브러리와 gcc가 필요함
    • Windows 빌드에는 clang이 필요함
  • 빌드는 플랫폼별 스크립트로 수행함
    • Linux: ./build.sh
    • Windows: .\build.bat
  • 빌드 결과물은 다음과 같음
    • Linux: blogtech
    • Windows: blogtech.exe
  • Linux에서는 ./install.sh로 설치 가능함
  • 기본 서버 실행 예시는 docroot 디렉터리를 만들고 --serve, --document-root=docroot, --skip-auth-check를 지정함
  • 기본 HTTP 서버는 127.0.0.1:8080에서 수신하고 docroot의 콘텐츠를 제공함
  • docroot 상태에서는 http://127.0.0.1:8080/ 접속 시 404가 반환됨

원격 파일 관리와 인증

  • BlogTech는 클라이언트 모드에서 파일을 서버에 업로드할 수 있음
    • --put으로 파일 업로드
    • --get으로 원격 파일 다운로드
    • --delete로 원격 파일 삭제
  • 같은 머신에서 클라이언트와 서버를 실행해도, 원격 머신의 서버에 대해서도 같은 방식으로 동작함
  • 리소스를 변경하는 요청은 HMAC/SHA256 서명으로 디지털 서명됨
  • 클라이언트와 서버는 같은 비밀키를 공유해야 하며, 관례적으로 admin.pwd 파일에 저장함
  • 비밀번호 파일은 --auth-password-file 옵션으로 지정함
  • 비밀번호 파일이 없으면 인증이 필요한 모든 요청이 거부됨
  • 빈 비밀번호도 거부됨
  • 개발 중에는 --skip-auth-check로 모든 요청을 인증된 것으로 처리할 수 있음
    • 이 옵션을 쓰면 --auth-password-file은 지정돼 있어도 무시됨
    • 주의해서 사용해야 함
  • 인증 헤더와 재전송 조건

    • 인증된 HTTP 요청에는 X-BlogTech- 헤더가 포함됨
    • X-BlogTech-Nonce: 클라이언트가 무작위로 선택한 토큰
    • X-BlogTech-Timestamp: 요청이 처음 서명된 시점의 UNIX 타임스탬프
    • X-BlogTech-Expire: 서명 시점부터 요청이 유효한 초 단위 시간
    • X-BlogTech-Signature: 요청 정보의 HMAC을 Base64로 인코딩한 값
    • 서명은 정규화된 요청 문자열을 만든 뒤 인증 비밀번호를 키로 사용해 HMAC/SHA256을 계산하고, 결과를 패딩 포함 Base64로 인코딩해 얻음
    • 서버가 이미 본 nonce는 메모리에 저장됨
    • 서버가 다시 로드되면 이전 nonce를 잊어버리므로, 만료되지 않은 요청은 재전송될 수 있음

가상 호스트와 디렉터리 매핑

  • BlogTech는 같은 서버에서 여러 웹사이트를 호스팅할 수 있음
  • 도메인은 --domain 옵션을 여러 번 지정해 설정함
  • BlogTech는 문서 루트 안에 도메인별 디렉터리를 생성함
    • default
    • websiteA.com
    • websiteB.com
  • 특정 호스트로 들어온 요청은 해당 호스트와 연결된 디렉터리를 참조함
  • 특정 폴더와 연결되지 않은 요청은 default 디렉터리를 참조함
  • 예시에서 websiteA.com, websiteB.com, other.com에 업로드한 파일은 각각 도메인 디렉터리 또는 default 디렉터리에 저장됨

HTTPS와 인증서 설정

  • HTTPS는 Linux에서만 지원
  • underlying HTTP 라이브러리인 cHTTP가 OpenSSL로 HTTPS를 구현하기 때문임
  • Windows에서는 클라이언트와 서버 모두 HTTP만 사용할 수 있음
  • HTTPS를 켜려면 다음 옵션이 필요함
    • --https-enabled
    • --cert-file
    • --cert-key-file
  • 기본 HTTPS 수신 주소는 127.0.0.1:8443
  • 수신 주소와 포트는 다음 옵션으로 변경 가능함
    • --https-addr=<addr>
    • --https-port=<port>
  • 개발 중에는 OpenSSL 명령으로 자가 서명 인증서를 만들 수 있음
  • 브라우저는 자가 서명 인증서를 쓰는 HTTPS 서버 탐색을 허용하지 않음
  • cURL은 --insecure 플래그를 써야 자가 서명 인증서 서버에 접근 가능함
  • 여러 인증서가 필요하면 --extra-cert를 추가로 전달할 수 있음
    • 기본 인증서는 --cert-file, --cert-key-file로 제공됨
    • 추가 인증서는 클라이언트가 특정 도메인을 요청할 때 사용됨
  • 긴 명령줄 옵션은 설정 파일로 옮길 수 있음

ACME 기반 자동 인증서 발급

  • ACME 프로토콜은 웹 서버가 인증기관에 인증서 발급을 자동 요청할 수 있게 함
  • BlogTech는 인증서 없이 HTTPS 모드로 시작한 뒤 인증서가 생성되는 흐름을 지원함
  • 운영 환경에서는 다음 조건이 필요함
    • HTTP 서버를 80번 포트에서 실행
    • 서버가 실행되는 머신과 도메인을 연결하는 DNS 레코드 생성
  • ACME는 --acme-enabled를 HTTPS 옵션과 함께 지정해 켬
  • 일반 HTTPS 모드와 달리, ACME 모드에서는 인증서가 존재하지 않을 것으로 기대함
  • 인증서 생성을 위해 다음 값이 필요함
    • --acme-domain
    • --acme-email
    • --acme-country
    • --acme-organization
    • --acme-agree-tos
  • 정상 처리되면 다음 파일이 생성됨
    • acme_key.pem: ACME 계정과 연결된 비밀키
    • cert.pem: 발급된 인증서 또는 --cert-file로 지정한 파일
    • key.pem: 인증서와 연결된 개인키 또는 --cert-key-file로 지정한 파일
  • 오류가 발생하면 메시지는 acme.log에 기록됨
  • --acme-domain을 여러 번 지정하면 여러 도메인을 ACME로 처리할 수 있음
  • 결과 인증서에는 지정한 모든 도메인이 포함됨

ACME 클라이언트 테스트

  • Linux에서 ACME 클라이언트를 테스트하려면 Docker를 설치하고 Pebble ACME server를 클론해야 함
  • docker-compose.yml에는 테스트 도메인을 host-gateway로 연결하는 extra_hosts 항목을 추가함
  • /etc/hosts에는 테스트 도메인을 127.0.0.1로 매핑함
  • Pebble은 docker compose up으로 시작함
  • BlogTech는 ./blogtech -s --config=misc/pebble_blogtech.conf로 테스트 인스턴스를 실행함
  • ACME 서버와의 상호작용은 stdout에 출력됨
  • 성공하면 acme_key.pem, cert.pem, key.pem이 생성됨
  • 인증서 갱신 테스트는 pebble/test/config/pebble-config.jsonvalidityPeriod 값을 바꿔 수행할 수 있음
  • --acme-force-renewal-period=<maximum duration in ms> 옵션을 사용하면 인증서가 만료되지 않았어도 갱신하도록 지시할 수 있음

설정 파일과 자동 로드

  • BlogTech는 임의 개수의 명령줄 인자를 설정 파일로 옮길 수 있음
  • 예를 들어 HTTPS와 ACME 관련 옵션을 blogtech_server.conf에 작성한 뒤 --config=blogtech_server.conf로 실행할 수 있음
  • 설정 파일 이름이 정확히 blogtech.conf이면 BlogTech가 자동으로 로드함
  • 이 경우 ./blogtech --serve처럼 실행할 수 있음
  • 암묵적 설정 파일 로드를 무시하려면 --no-config를 사용함

크래시 로그와 systemd 데몬

  • 서버 모드에서 BlogTech가 크래시하면 crash.bin 파일이 생성됨
  • 다음 서버 시작 시 crash.bin은 사람이 읽을 수 있는 스택 트레이스인 crash.log로 변환됨
  • 주소를 심볼 이름이나 줄 번호로 변환하는 과정은 다소 불안정할 수 있음
  • BlogTech는 systemd 데몬으로 설치할 수 있음
  • 예시 blogtech.service/root/blogtech/blogtech -s를 실행하고, 실패 시 재시작하며, 작업 디렉터리를 /root/blogtech/로 설정함
  • 서버 옵션은 /root/blogtech/blogtech.conf에서 자동 로드됨
  • 서비스 파일을 /etc/systemd/system/에 복사한 뒤 다음 명령으로 활성화하고 시작함
    • systemctl daemon-reload
    • systemctl enable blogtech
    • systemctl start blogtech
  • 서비스 관리는 systemctl start, systemctl stop, systemctl restart, journalctl -u blogtech로 수행함

댓글과 토론

Hacker News 의견들
  • 리버스 프록시 불필요”라는 부분은 늘 의아했음
    리버스 프록시가 특별히 도움이 되는 이유가 없으면, 임베디드 Jetty 앱을 앞단 없이 인터넷에 바로 노출하곤 했고 문제는 없었음
    인프라나 보안 담당자들은 왜 앞에 nginx를 두지 않느냐고 묻지만, 이유를 물으면 보안이나 성능을 두루뭉술하게 말할 뿐 구체성이 부족함. 가장 구체적이었던 답은 slow loris였는데, 그건 이미 오래전부터 큰 문제가 아니었음
    리버스 프록시는 집단적으로 카고 컬트가 된 건지, 아니면 일반적으로 적용되는 좋은 이유를 내가 놓치고 있는 건지 궁금함

    • 내게 리버스 프록시는 원본 서버가 오직 애플리케이션 제공만 하게 해줌. TLS 종료, 부하 분산, URL 재작성, 필요 시 WAF 같은 보안 기능은 전부 리버스 프록시에서 처리할 수 있어 역할 분리가 됨
      전체적으로 원본 서버를 보호하고 관련 있는 트래픽만 받게 할 수 있음. 고객에게 커스텀 도메인을 제공하는 경우에도 고객 DNS가 원본 서버가 아니라 리버스 프록시를 가리키므로, 필요하면 원본 서버를 바꿔도 고객 DNS 변경을 걱정하지 않아도 됨
    • 홈랩에서 여러 서버 프로그램을 돌리고 있음
      각각 다른 포트에서 실행되지만, 전부 서로 다른 URL로 공개 접근 가능하게 하고 싶고 인터넷에는 443 포트만 노출하고 싶음
      각 도메인의 TLS 인증서 자동 갱신도 원함. 앞의 요구에는 리버스 프록시가 필요하고, Caddy는 둘 다 해줌
      단일 서버만 운영하고 그 서버가 TLS 종료까지 한다면 리버스 프록시는 꼭 필요하지 않음
    • 대부분의 배포에서 리버스 프록시의 성능 영향은 무시할 만하고, 설정도 미리 준비해둠
      나중에 TLS 종료, URL 재작성, 기타 작업을 큰 수고 없이 붙일 수 있어서 습관처럼 쓰게 됐고 지금까지는 그 습관이 보답해줬음
    • 웹 애플리케이션 앞에 놓이는 여러 종류의 서버에 적용되는 답을 하자면, 보안과 성능 양쪽에서 추가 장비가 도움이 되는 방식이 몇 가지 있음
      보안 측면에서는 전체 시스템에서 가능한 한 코드가 적고 견고한 운영체제를 쓰고 싶음. 프록시형 앱은 웹/애플리케이션 서버보다 훨씬 단순할 수 있고, 들어오는 트래픽을 필터링하거나 입력을 검증하거나 더 안전하고 빠르게 파싱 가능한 형태로 바꿀 수도 있음. OpenBSD, GenodeOS, INTEGRITY-178B처럼 공격하기 어려운 운영체제에서 돌릴 수도 있음
      가용성 측면에서도 부하 분산, 모니터링, 복구를 이런 시스템에 두는 편이 안전한 경우가 많음. 애플리케이션 서버가 더 자주 죽을 수 있기 때문임
      성능 측면에서는 단순하고 집중된 앱을 고도로 최적화할 수 있다는 장점이 먼저 있음. 이후에는 CPU나 PCI 하드웨어 가속기로 압축이나 암호화를 빠르게 할 수 있는데, 흔히 오프로딩이라고 부름. 비용 효율이 가장 좋은 구성은 많은 범용 서버가 소수의 고가 오프로딩 서버의 이점을 받는 형태임. 일부는 부하 분산으로 들어오는 트래픽을 가장 잘 처리할 서버로 보내 비싼 자원 사용을 줄임
    • 모든 서버에 대한 일반론이 있다고 보지는 않음
      최소 구성에서는 필요 없지만, 단일 호스트 운영 환경에서도 롤링 릴리스, 압축, TLS, 빠른 정적 파일 제공, 잠재적인 A/B 테스트를 가능하게 해줌
      요청과 서버 사이의 간접 계층은 꽤 유용할 수 있음
  • 멋짐. 나도 예전에 직접 C 웹 서버를 작성했고, 아래에 소스가 있음. 한동안 상용 웹사이트를 돌리기도 했음
    HTTP/1.1 웹 서버를 얼마나 작고 가볍게 만들 수 있는지 놀라움. 그 상용 사이트는 RAM 128MB와 CPU 1개짜리 머신에서 돌아갔고, 폐쇄 소스의 대화형 웹 기반 채팅 시스템으로 영국 학교 상당수를 정기적으로 서비스했음. 다만 그건 인터넷이 지금보다 덜 적대적이던 20년 전 얘기임
    글에서는 봇이 훌륭한 퍼저 역할을 한다고 했지만, 그래도 실제 퍼징도 조금은 해보는 게 좋다고 봄
    http://git.annexia.org/?p=rws.git;a=tree
    필요 항목:
    http://git.annexia.org/?p=c2lib.git;a=tree
    http://git.annexia.org/?p=pthrlib.git;a=tree

    • 이런 크기의 웹 서버를 돌리면서 적대적인 인터넷을 너무 걱정하지 않으려면 Rust가 좋은 선택임
      내 웹사이트 https://blessed.rs는 찾을 수 있는 가장 작은 VM이어서 RAM 256MB짜리에서 돌고 있지만, 보통은 약 60MB만 사용함
    • 이건 내가 만든 작고 가벼운 HTTP/1.0 웹 서버보다 훨씬 실용적으로 보이지만, rws가 그만큼 작고 가볍지는 않을 것 같음: http://canonical.org/~kragen/sw/dev3/server.s http://canonical.org/~kragen/sw/dev3/httpdito-readme
      정말 놀라웠던 건 메모리 맵이 4KB 페이지 5개뿐이면 Linux의 fork가 엄청 빨라진다는 점이었음
  • 재미로 여가 시간에 시작한 작은 프로젝트인데, 여기서 좋아할 것 같았음 :)

    • 오래된 HTTP 서버 버그와 CVE를 훑어보면서 내 코드에도 영향을 줄 수 있는지, 어떻게 고칠 수 있는지 보는 게 흥미로운 연습이 됨. 이런 걸 직접 만들어보는 건 재미있음
    • 마침 C11 동시성 API를 만져보고 싶었고, C++에서 오던 입장에서 그 구조들이 C에서 어떻게 작동하는지 궁금해서 서버 비슷한 걸 써보고 싶었음
  • 대단함. 이런 식으로 가장 낮은 수준의 시스템 API를 써서 최소한의 서비스를 띄우는 건 정말 만족스럽다고 예전부터 생각했고 지금도 그렇게 봄
    거의 마법 같고, 실제 트래픽을 처리하는 걸 보면 더 그렇다. 평범한 poll()이 저 정도 수치를 낼 수 있다는 건 조금 놀라웠지만, 내가 그 수준에서 이벤트나 벤치마크를 다룬 지 오래돼서 그런 듯함
    연결별 함수와 관련 구조체, 배열로 연결을 관리하는 방식, poll 파일 디스크립터 배열도 좋음. nginx, Redis, memcached처럼 높은 처리량으로 알려진 여러 오픈소스 패키지에서 하는 방식이 떠오름

    • 대학에서 C/C++를 다루면서 머리가 터지는 느낌이었음. 공학, 역사, 문화, 언어학 등 내가 좋아하는 것들이 조금씩 다 들어 있는, 아주 특수하고 겸손해지는 경험이었음
      누구나 가능한 모든 언어, 프로그래밍 언어든 자연어든 알고 시도해봐야 한다고 생각하게 됨. 어떤 언어로 생각한다는 것은 독특한 경험임. 서로 다른 맥락이 모든 것을 다르게 느끼게 하고, 결국 비슷한 것을 해도 관점이 바뀜
      예를 들어 Linux나 Git의 본질을 정말 이해하려면 그 언어를 말하고, 번역에서 보통 사라지는 뉘앙스를 이해해야 함. 러시아어에서 “숲”이라는 단어의 진짜 주관적 의미를 이해하려면 러시아어를 말하고 이해해야 하는 것과 비슷함
      맥락이 관점을 바꾸고, 때로는 모든 것을 바꿈
    • 완전히 동의함. 그리고 실제로 써보면 더 만족스러움. 이제는 이메일 프로토콜도 궁금해지기 시작함
      나도 평범한 poll()이 저 정도로 버틴다는 게 놀라웠음. 언젠가는 epoll로 갈 거라고 생각했는데, poll()이 아주 잘 동작하고 있음
    • 사람들이 잊는 것 같은데, 훌륭하고 멋진 모든 추상화도 핵심에서는 결국 똑같은 일을 함. 소켓을 열고, 읽고, 쓰는 것임
      해 아래 새것은 없음
  • 이것도 흥미로울 수 있음: https://news.ycombinator.com/item?id=27431910
    2024년 기준 sqlite.org의 althttpd 인스턴스는 하루 50만 건 이상의 HTTP 요청, 초당 약 5~6건을 처리하고, 하루 약 200GB 콘텐츠, 약 18Mbit/s를 월 40달러 Linode에서 제공함. 이 머신의 부하 평균은 보통 0.5 근처에 머무름. HTTP 요청의 약 19%는 여러 Fossil 소스 코드 저장소로 가는 CGI임

    • 이 글에서 큰 영감을 받았음. 이런 게 실제로 가능하다는 걸 깨닫게 해줌
  • C 앱을 작성하고 싶지만 공개 인터넷에 직접 닿는 부분을 직접 쓰는 게 불편하다면, Kore가 괜찮은 프레임워크임
    ACME 인증서 관리, Pgsql, curl, 웹소켓 같은 편리한 내장 기능이 있음
    기본적으로 모듈을 빌드해서 실행하고 조합할 수 있으며, Lua/Python과 C를 섞는 것도 가능함
    https://kore.io/

  • 드디어 첫 페이지에 올라와도 죽지 않는 웹사이트가 나왔음

    • 앞에 CDN이 있으면 어떤 사이트든 그렇게 할 수 있음
      이 프로젝트가 훌륭하지 않다는 뜻은 아니지만, 운영 환경에서 이런 걸 정말 중요하게 보고 대부분 정적 콘텐츠를 제공한다면 그냥 CDN을 쓰면 됨. 직접 작성하는 거의 무엇보다도 늘 더 좋은 성능을 낼 것임. 다만 재미가 없을 뿐임
    • 링크가 GitHub로 가는 것 아닌가? 이 댓글이 조금 헷갈림
      프로젝트는 깔끔하고 멋지지만, 대부분은 GitHub로 가지 웹페이지를 보여주는 링크로 가지 않을 것 같음. 뭔가 놓친 건가?
  • “내 도구를 직접 만드는 걸 즐기고, 모든 게 실전 검증돼야 한다는 말을 듣는 데 좀 지쳤다. 그래서 죽으면 뭐 어떤가? 버그는 고치면 된다 :^)”라는 부분이 좋음

  • C 코드 3.4k 줄만으로 완전한 HTTP와 HTTPS 서버라니? 사양을 완전히 준수하려면 솔직히 훨씬 더 많이 필요할 줄 알았음

    • HTTP/1.1은 사양 대부분을 무시하면 아주 단순함
      GET 요청만 받고 응답에 Content-Length를 설정하면 사용자 에이전트 99%에는 충분함. Transfer-Encoding과 바이트 범위 헤더를 처리하는 것도 코드가 크게 늘지는 않음
      HTTPS는 TLS 소켓 위의 HTTP일 뿐이고, 직접 암호화를 구현하지 않는다면 딱 그 정도 추상화 수준이어야 함. 재미있고 실제로 그렇게 나쁘지 않음
    • 이 정도면 적당해 보임. OpenBSD의 httpd(8) [1]도 문서까지 포함해 현재 15,000줄이 조금 안 됨
      기능 몇 개를 빼고 몇 가지 가정을 두면, 이 프로젝트처럼 5,000줄대에 들어간다고 해도 놀랍지 않음
      $ wc -l * 결과 총 14815줄임
      [1]: https://man.openbsd.org/httpd.8
    • 내 실험 중 하나에서 데이터 수집 라이브뷰를 제공하려고 간단한 임베디드 C 웹 서버를 썼는데 250줄 미만이었음
      물론 공개 인터넷에 올리지는 않을 것이고 HTTP/1.1의 아주 작은 부분만 구현했지만, 동작하며 초기화 때만 malloc이 필요함
    • 이런 크기의 HTTP/1.1 서버는 몇 개 더 있음: https://www.acme.com/software/thttpd/benchmarks.html
  • C로 작성된 블로그/웹 서버에 대한 Chaos Communication Congress 발표가 떠오름
    불변 저장소, 권한 강등, 블로그가 TLS 인증서에 접근하지 못하게 하는 등 보안 기능을 많이 넣은 내용이었음: https://www.youtube.com/watch?v=TaE28fJVPTk