nginx + ngx_pagespeed

꽤나 예전에 동아리 서버에는 적용해봤는데 — 동아리 서버는 apache 웹서버를 써서 구글에서 바이너리로 배포하는 mod_pagespeed 를 썼지만 — 이 블로그가 쓰는 웹 서버는 nginx를 써서 적용 못하고 있었다.
Nginx 는 다 좋은데 동적으로 로드 가능한 모듈을 지원하질 않아서 소스에 넣고 빌드해줘야하는 것. 개인 웹 서버라 개발툴 없이 돌리는 상태를 유지하고 싶어서 회사 개발머신을 빌려 데비안 패키지만 만들고 옮겨서 설치하는 것으로. 아래는 그 기록.

빌드 기록

빌드 과정은 소스에서 ngx_pagespeed 빌드하기 를 따라하되 nginx 소스코드와 빌드 자체는 ubuntu 14.04의 debian 빌드 스크립트를 이용했다.

소스 코드 얻기

nginx 소스 패키지와 ngx_pagespeed 소스 타르볼, 그리고 PSOL 타르볼을 얻는다:

# nginx (1.4.6) 소스코드 + 빌드스크립트
$ apt-get source nginx

# ngx_pagespeed 릴리즈 아카이브
$ wget https://github.com/pagespeed/ngx_pagespeed/archive/release-1.9.32.10-beta.zip

# PSOL
$ wget https://dl.google.com/dl/page-speed/psol/1.9.32.10.tar.gz

# 압축 해제 및 필요한 구조로 변경
$ tar -zxf 1.9.32.10.tar.gz
$ unzip release-1.9.32.10-beta.zip

# PSOL 을 ngx_pagespeed 밑으로
$ mv psol/ ngx_pagespeed-release-1.9.32.10-beta/

# ngx_pagespeed 를 모듈 디렉터리로 이전
$ mv ngx_pagespeed-release-1.9.32.10-beta/ nginx-1.4.6/debian/modules/

nginx 빌드룰 수정

데비안 패키지의 빌드룰은 “{패키지-버전}/debian/rules” 에 있다. 여기선 nginx-1.4.6/debian/rules 를 열어 nginx-full 에 해당하는 config.status.full 항의 맨 뒤에 “--add-module=$(MODULESDIR)/ngx_pagespeed-release-1.9.32.10-beta \” 를 추가해서 아래와 같이 수정하다:

config.status.full: config.env.full
  cd $(BUILDDIR_full) && ./configure  \
      $(common_configure_flags) \
      --with-http_addition_module \
      --with-http_dav_module \
      --with-http_geoip_module \
      --with-http_gzip_static_module \
      --with-http_image_filter_module \
      --with-http_spdy_module \
      --with-http_sub_module \
      --with-http_xslt_module \
      --with-mail \
      --with-mail_ssl_module \
      --add-module=$(MODULESDIR)/nginx-auth-pam \
      --add-module=$(MODULESDIR)/nginx-dav-ext-module \
      --add-module=$(MODULESDIR)/nginx-echo \
      --add-module=$(MODULESDIR)/nginx-upstream-fair \
      --add-module=$(MODULESDIR)/ngx_http_substitutions_filter_module \
      --add-module=$(MODULESDIR)/ngx_pagespeed-release-1.9.32.10-beta \
            >$@

데비안 패키지 생성

$ cd nginx-1.4.6/
$ debuild -uc -us -rfakeroot -b
$ ls -1 ../*.deb
../nginx_1.4.6-1ubuntu3.3_all.deb
../nginx-common_1.4.6-1ubuntu3.3_all.deb
../nginx-core_1.4.6-1ubuntu3.3_amd64.deb
../nginx-core-dbg_1.4.6-1ubuntu3.3_amd64.deb
../nginx-doc_1.4.6-1ubuntu3.3_all.deb
../nginx-extras_1.4.6-1ubuntu3.3_amd64.deb
../nginx-extras-dbg_1.4.6-1ubuntu3.3_amd64.deb
../nginx-full_1.4.6-1ubuntu3.3_amd64.deb
../nginx-full-dbg_1.4.6-1ubuntu3.3_amd64.deb
../nginx-light_1.4.6-1ubuntu3.3_amd64.deb
../nginx-light-dbg_1.4.6-1ubuntu3.3_amd64.deb
../nginx-naxsi_1.4.6-1ubuntu3.3_amd64.deb
../nginx-naxsi-dbg_1.4.6-1ubuntu3.3_amd64.deb
../nginx-naxsi-ui_1.4.6-1ubuntu3.3_all.deb

…적당히 기다리고 나면 탑 디렉터리에 .deb 파일 수 개가 생긴다. 위에서 빌드 룰 수정한 nginx-full 패키지를 가져가서 서버에 설치.[1] 빌드 시간을 줄이고 싶다면 debian/control 을 수정하는 것도 (다음 번엔) 고려해야겠다.

설정하기

ngx_pagespeed : Nginx의 PageSpeed 모듈 :: Outsider’s Dev Story 를 참조해서 설정했다. 기본 값 + JavaScript 비동기 로딩 정도 설정함.

총평

Google PageSpeed Insight 점수가 15점 쯤 올랐다 (…). 행복하군 (야)
대신 앞으론 ubuntu 쪽 패키지 업데이트 / Google 쪽 릴리즈 마다 새로 빌드해야한다 (이쪽은 전혀 달갑지 않다…)

PS. (아마도) GPL인 debian/rules 파일을 일부 인용했는데, 이 글 라이센스를 CC-BY 대신 GFDL로 해야하나? 아니면 공정 사용이니 그냥 CC-BY 해도되나?(…)

  1. 빌드할 때 좀 괴롭더라. -j8 주니 빌드 실패한다. 빌드 스크립트에 무슨 삽질을 해놓은걸까? []

TLS: SHA1 해시의 종말이 다가오고 있다

ars technica: SHA1 암호학적 해시가 연말 전에 (쉽게) 무력화된다 라는 글이 올라왔다. (2015-10-08)

저 글에서도 나오고, 이 블로그의 이전 글에서 링크한 Bruce Schneier의 2012년 예측 에서는 2018년이면 조직범죄자들 정도의 재정이면 충분히 깰 수 있는 수준이 될거고 예측했었다. 하지만 새로 나온 논문에 따르면 이미 현재 기준으로 $75k ~ $125k 정도면 충돌을 찾을 수 있다 라는 것. (이전 예측치는 2018년에 $175k 정도)

즉, 지금이라도 어느 정도의 재정 규모가 되는 정부건 정부 단체건, 혹은 조직범죄자들이건 이걸 깨려는 시도를 할 수 있는 수준으로 계산 비용이 내려와 있다는 얘기. 다행히도 언급된 논문에서 찾은건 chosen prefix attack 은 아니어서, 당장 인증서를 위조해내는 정도는 아님. 하지만 여태까지 그래왔고, 앞으로도 그렇겠지만 공격은 항상 향상되지 악화될리가 없다. 그러니 근미래에 뚫린 상황 — $100k 수준에서 chose-prefix attack이 가능해지는 상황 — 에 진입할지도 모른다. (Google 은 2017년 1월부터 SHA1 해시를 쓴 인증서를 안전하지 않은 것으로 간주하기로 했다(Chrome 41이후) )

여하튼 해야할 일:

  • 사용 중인 인증서가 SHA-1 해시로 서명되어 있는가? (직접 쓰는 인증서, intermediate 인증서 모두) 그러면 바꿀 것. 아마 이미 발급된 인증서에 대해서 무료(?) 재서명을 해줄지도 모름
  • SHA-1 해시로 서명된 사이트에 접속하고 그 사이트와 안전하게 통신하는게 중요하다면 그 사이트 사용을 멈추거나 (예를 들어 국민은행?) 바꾸라고 요구해야 한다
  • 조만간 주요 웹 브라우저에서 SHA-1 해시를 이용해서 서명한 인증서들을 안전하지 않다 라고 표시하는 업데이트가 나올테니 일찍일찍 설치해둘 것

정도를 해야.

아 그리고 SHA-1 해시를 root 인증서 에 쓴 건 상관없다. 아침에 일어났다가 불현듯 이게 떠올라서 — 이 블로그가 쓰는 인증서의 Root CA는 SHA-1 서명; 구글도 같은 Root CA 씀(…) — 검색해보니 Gradually sunsetting SHA-1 이란 글 발견. 해당 부분을 발췌하면,

Note: SHA-1-based signatures for trusted root certificates are not a problem because TLS clients trust them by their identity, rather than by the signature of their hash.

즉, root 인증서의 경우 공개키(pub-key)를 가지고 있는거고, (자가) 서명한 signature 자체는 검증할 일이 없기 때문에 상관없다는 것. (대조할 대상 이 아예 없으니까)

은행은 왜 이렇게 오래된 인증서 알고리즘 / 암호화 알고리즘을 쓰는 걸까

Disclaimer: 작성자 본인은 암호학 전문가가 아닙니다. 이글에선 무엇도 보장하지 않고 이글을 참고해서 하는 일에 대해선 어떤 책임도 지지 않습니다.

국민은행이 모던 브라우저에서 인터넷 뱅킹이 된다기에 시험 해 봄. 근데 들어가자마자 뜨는 워닝이 너무 우울해서 몇 가지를 확인.

국민은행 접속 시 뜨는 경고

인증서가 SHA1 해시를 이용

인증서가 유효한지 확인하기 위해서는 해당 인증서가 유효한지 확인하기 위해 인증서의 해시 값을 구하고, 이를 서명해둔다. 이 때 암호화 해시 함수; cryptographic hash function 을 이용하는데, 여기서는 SHA1 해시 함수를 쓰고 있더라.

현재 구글이나 모질라에서는 SHA1 대신 그 보다 안전한 다름 함수를 쓸 것을 권한다. Bruce Schneier는 2012년에 2015년이면 약 USD 700k 정도면 충돌하는 다른 해시 값을 찾을 수 있다고 추정 하기도 했다. 즉, 현실적으로 저 해시에 해당하는 인증서를 위조(…)할 가능성이 생긴다. 게다가 인증서가 2015년에 생성한 것이던데.

암호화 알고리즘이 구닥다리 (no PFS!)

지원하는 암호화 알고리즘 조합이 키 교환은 RSA만 이용하고 — 그래서 키가 털리면 이전 모든 통신을 복호화해 볼 수 있고 — 메시지 암호화 알고리즘도 AES-128/256-CBC 나 RC4, 3DES-EDE (…) 를 지원한다. 세션 동안만 유효한 암호화 키 (ephemeral key)를 쓰지 않는다. 으으 지금은 2015년 같은데. 게다가 쓰지말라고한지 2년은 된 RC4?

게다가 아직 SSL v3가 남아있다. 혹시 POODLE이라고 들어는 보셨는지… 다행히 TLS_FALLBACK_SCSV 를 써서 TLS v1 이상을 쓰는 브라우저로 접속하면 당장 문제는 아니긴하겠지만.

그들을 위한 변명 …

SHA1 알고리즘의 대체인 SHA2의 호환성을 확인하자면, 클라이언트가 Windows XP SP3 (!!) 이후면 문제가 없다. (DigiCert의 해당 안내문 ) 아직도 Windows XP SP2 + IE6를 쓴다면 이런 인증서를 써야할 수…는 있다.

RC4의 문제도 실제로 이게 선택되는 조합은 Qualys SSL LABS 결과엔 없다. (그럼 걍 빼지…).

SSL v3는 Windows XP + IE6 를 지원하려면 필요하긴 하다.

근데 이것 때문에 FS를 위한 키교환 알고리즘을 못 쓸 이유는 없는 점은 변명의 여지는 전혀 없음. 그리고 이런 사용자 (Windows XP + IE6)는 은행이 책임져 주지도 않는데 차단 할 일이지, 이걸 위해서 다른 사용자가 더 약한 암호화/인증서를 쓸 이유는 안된다.

기왕 모던 브라우저 지원하는 김에 버릴 건 버리고 가는게 맞지 않을까?

카톡에서 쓰는 것으로 보이는 user-agent 가 눈에 띄기에

얼마 전부터 대략 이렇게 생긴 user-agent 가 웹 서버 접근 로그에 남는다:

*.*.*.* - - [07/Sep/2015:??:??:?? +0000] "GET /blog/archives/3454 HTTP/1.1" 200 9925 "-" "facebookexternalhit/1.1;kakaotalk-scrap/1.0;"

IP 주소에 대한 whois 정보 역시 Daum 소유의 것으로 나옴.
Facebook 용으로 og:* metatag 류의 태그를 지정하는 경우 가 왕왕 있어서 그런 정보를 쉽게 가져가려고 facebook 외부 접근 에이전트 (facebookexternalhit/1.1) 호환이라고 내보내는 모양.

카카오 톡에 링크 공유하면 미리보기 띄우는 용도일듯? (Facebook 역시 그런 용도 + alpha 로 저걸 쓰는 듯 하니..)

블로그 서비스 이전 준비 중

이 블로그는 만든 이래로 계속 동아리 서버 신세를 지고 있는데 이젠 옮길 때가 된 듯하여 — SSL + SNI 문제라거나 — 옮길 곳을 찾는 중.

대략 EC2 도쿄리전 아니면 GMO 쪽으로 옮기지 않을까 싶은데; (대략 월 2만 이하 가격; 트래픽은 크지 않음…)
뭔가 써 보신 분들은 추천 좀.

대략 다음과 같은 부분을 생각 중:

  • https-only w/ HSTS
  • .kr / .net / … 도메인에 대해서 SNI 설정이 가능해야 함 (.kr …)
  • VM 접근 권한이 다 있으면 좋지만, 안되면 스태틱 파일만 내보내는 것도 생각 중 (하지만 disqus 류는 느려서 괴로울지도)
  • 한 달 사용료 2만 안쪽으로…
  • DNS 서버 제공해주면 좋고
  • 싼 인증서 패키지 (…이건 약간 카테고리가 다른 듯)

하지만 과연 나의 귀차니즘을 이길런지는.

Scrapy로 웹사이트 크롤링 해보기

웹 사이트 몇 개(=네xx카페)를 지속적으로 크롤링 할 일이 생겨서 몇 가지 라이브러리를 뒤적여봤다.

처음 시도한 것은 selenium. 하지만 UI 요소에 의미있는 텍스트가 없거나, 반복해서 나타나는데 문맥 의존적으로 해석하게되면 완전 노가다. 그리고 내가 아는 범위 내에선 서버 데몬을 따로 띄우지 않고는 안되는 녀석이라 따로 돌리기 불편해서 중도 포기.

다음으론 항상 잘 써먹던, 비교적 규모가 작은 웹사이트에서 잘 써먹던 requests + html5lib. 근데 이것도 UI 요소에 의미있는 클래스나 아이디가 없어서 불편하긴 하더라. 그리고 동적으로 링크를 발견하고, 이걸 다시 특정 형태로 크롤링하게 하려니 작업량이 많아서 작업 중단.

마지막으로 scrapy를 썼고, 이런 류의 작업이 맞춘 툴이어서 이걸로 완성.
다음과 같은 방식으로 동작한다:

1. 시작 url 지정
2. 페이지를 긁고, 이것에 대한 parse 콜백
2-a. HTTP 요청에 대한 응답을 xpath로 뒤져서 적당한 값을 읽기
2-b. 추가로 크롤할 url을 yield. (별도로 파싱할 때 쓸 콜백도 지정)
2-c. 원하는 값을 찾으면 이걸 미리 지정한 아이템 타입으로 yield.
3. 처리할 url이 남으면 2로.

2-b, c를 혼용할 수 있는게 정말 편했다.

더불어, 실재로 작업을 하는 2-a~c만 쓸 수 있는 scrapy shell {URL} 명령이 무진장 유용했음. 해당 url에 대해서 딱 콜백 시작 부분까지 진행한 ipython shell이 뜨더라. 여기서 이거저거 테스트해보고 파싱 코드를 작성하면 되더라.

그리고 작업하다가 깨달음을 하나 얻었는데, 모바일 페이지를 크롤링하는게 몇 배 쉽다. 노가다 좀 하면 requests + html5lib 로도 가능할 듯.

세 줄 요약:

– scrapy가 웹 크롤링 작업의 반복적인 부분을 줄여주고 꽤 편하게 만들어주더라
– scrapy shell {URL or file path} 완전 편함
– 모바일 페이지가 있고, 필요한 정보가 다 나온다면 이쪽을 크롤링하는게 쉽다

신규 웹 서비스 뒷조사(?)해보기

며칠 전 트위터 타임라인에서 https://urigit.com 이란 서비스에 대해 초대권 받는다는 글이 보여서 뒷조사(?)한 후기.

내가 쓰는 방식이 상식적인 방법인진 모르겠지만, 여하튼 대략 다음과 같은 걸 읽어본다.

1. DNS 주소 정보

urigit.com은 올해 1/19에야 등록이 되었고 (그러니까 최종적인 서비스 명칭은 이 시점에야…),
등록자는 애조로 스튜디오란 이름이었다.

소개 페이지의 내용도 그렇고 메일 주소가 daum.net 인걸로 봐선 다음 제주 본사에 근무 중인 개발자가 주축인 것으로 보인다.

그리고 DNS 서버는 AWSDNS-* 이다. 서버도 AWS 위에서 돌리고 있음.
다음에서 직접 준비한 서비스가 아니거나 다음이 AWS를 테스트베드로 쓰고 있거나인듯. (누가 다른 정보 좀…)

근데 이 정보는 아예 막혀있는 경우도 많다. godaddy나 gabia 등도 registar 주소만 보이는 경우가 더 많음…

2. SSL 인증서 정보

인증서가 1/30에 발행된듯하다. 아마 twitter  계정으로 해당 초대권을 알리기 시작한 날과 무관하지 않은 듯.

3. Twitter 등의 SNS 페이지

이건 큰 참고는 안되고, 대략 public하게 행동한게 언젠지 알게 해주는 정도. 대략 DNS 등록한 날짜랑 큰 차이가 없음.

 

사실 이런 걸 까본 이유는 다음과 같은 점이 궁금해서:

1. git 호스팅 서비스가 큰 의미가 있는가?

한국에선 최초로 하는 서비스 일 것 같다. 근데 git 기반이면 지연시간이 짧다는 것 하나만으론 큰 의미가 없을 것 같다.

git 의 remote operation을 할 일 자체가 자주 없는데 그럼에도 호스팅 서비스가 의미가 큰 가?
특히 github, bitbucket, google-code 같은 다른 서비스들이랑 비교해서?

2. 사업 모델이 뭘까?

단순 호스팅 서비스를 판매하는 거라면 이게 의미가 클까? 상대적으로 연동할 수 있는 툴 수도 제한적일텐데.
github 처럼 연동 도구를 선점하거나, bitbucket처럼 배후에 아틀라시안을 깔고 있는 건 아닐텐데. (배후에 있는 회사가 다음이면 잘 모르겠다)

github 처럼 설치 버전을 판다면 이게 몇몇 스타트업들이 이용하는 gitlab이나, 규모가 좀 되는 회사들(예를 들어 카카오?)이 도입하기도 하는  github enterprise보다 큰 가치가 무엇일까?

WebApp 안에 다른 WebApp 넣어서 보여주기

최근 몇 일간 이미 만들어진 WebApp A안에 다른 WebApp B이 하나의 페이지처럼 들어가 보이도록 하는 작업을 했다. 사용자는 A에는 접근할 수 있지만, B에는 (대부분) 접근할 수 없는 상태.

정석(?)은 사실 이런 식으로 해야 할 것 같지만:

  • B를 RESTful API 혹은 어떤 web API를 갖는 서비스로 작성하고 (Service B)
  • B의 RESTful API를 보여주는 view에 해당하는 WebApp을 작성하고 (App B)
  • A는 ‘Service B’를 써서 특정 페이지를 만든다

하지만 그렇게 하지는 않고, 여름에 인턴으로 잠시 일했던 puzzlet군이 사용했던 방법을 좀 고쳐서 썼다.

Web B가 html5로 작성한거라서 http proxy 비슷한거 만들기는 편했다.

인증

다행히 이 두 웹앱은 인증 서비스는 공유한다. 그래서 A에서 인증한 shared-secret을 가지고 만든 데이터를 B에게 주고 이걸로 인증하게 했다.
그리고 B에 접속할 떄 사용하는 세션 키를 A의 세션에 저장하고 꺼내서 쓰게 했다.

페이지 접근

A 의 특정 URL 밑에 오는 것은 B의 URL로 접근하게 했다. 다만 일부 접근할 수 없는 URI에 대한 처리는 있다.
이런게 없었다면 그냥 reverse proxy쓰거나 하면 될 듯도.

web_app_A_host/page_B/… 이면 실제론  B의 페이지에 접근하게 했다. 실제론 URI가 완전히 맵핑되는 건 아니고, 특정 변환을 해주긴해야 해서 reverse-proxy를 쓰진 않았다.

HTTP 응답 rewrite

http 응답은 몇 가지 조건을 걸고 rewrite했다.

1.  http 헤더에 Location 필드가 있는 경우

30x나 201, 202인 경우 이게 붙어서 오는데, 이번 경우엔 201/202는 안 써서 그냥 30x에 Location field를 rewrite하게 했다.

2. html 페이지

href, src 등으로 오는 것 전부 rewrite.

그리고 나서 css, js, 실제 내용에 해당하는 부분을 찾았다. 그리곤 다음과 같이 합쳐서 내보냈다. html5lib이 xpath 로 DOM에 접근할 수 있어서 엄청 간단히 했다.

rewrite-html

3. css 추가 처리 

CSS의 경우 골치 아픈 문제가 생기더라. B에서 온 CSS 셀렉터에 해당하는게 A 안에도 있어서 레이아웃까지 깨지고 있었다. Orz.

그래서 위에서 한 것처럼 특정 class 밑에 B의 내용이 오게 강제하고, B에서 오는 모든 css 에는 셀렉터마다 (…) 맨 앞에 저 embedded가 붙게 했다.
cssutils로 해석한 다음 각 셀렉터를 하나하나 업데이트하는 코드를 짰다.

약간 느려지긴 하던데 어차피 저건 정적으로 캐싱할 수 있으니까 신경 끄기로 (야).

 

결론(?)

  • http 요청하는 클라이언트 흉내내기는 재밌더라
  • CSS 때문에 귀찮아…
  • B가 사실 상 페이지 1개짜리 앱이니 그냥 했지, 안 그랬으면 RESTful API로 서비스 분리하는게 훨씬 속 편할 것 같다.