개요
pypiserver는 Python 패키지를 업로드할 수 있는 인덱스 구현체 중 하나다. pip3 install <package name>을 실행하면 어딘가에서 패키지를 다운로드할 수 있는데, 이 과정에서 패키지 인덱스에서 검색하고 다운로드할 수 있게 된다. 패키지 개발자들은 보통 개발한 패키지를 공용 패키지 인덱스인 https://pypi.org/ 올린다. 그러나 개인이나 조직에서 개발한 패키지들을 공용 패키지 인덱스에 올려서는 곤란하다. 이를 위해 개인이나 조직 내부에서 사용할 수 있는 사설 패키지 인덱스를 설정하려고 한다.
설치 환경
여러 가지 방식으로 설치할 수 있는데, Ubuntu 22.04 + Apache2 + wsgi 환경에서 설정하려고 한다. Apache2는 HTTPS를 사용한다.
Apache2 + mod_proxy는 어떤가요?
ProxyPass와 ProxyPassReversed Directives를 사용하면 동작은 한다. pypiserver에서 자체적으로 HTTPS를 지원하지는 않으므로 HTTPS 연결이 들어오면 HTTP로 Reverse Proxy를 설정하게 된다. 그런데 pypiserver에서 만들어내는 HTML 출력에는 http:// 로 들어가기 때문에 이 출력을 https:// 등으로 적절하게 변환해줘야하는 문제점이 생기게 된다. pypiserver에서 링크마다 base url을 설정할 수 있는 기능은 지원하지 않는 듯하다. 그래서 이 방법은 비추천한다.
디렉토리 구성
디렉토리 구성에는 정답은 없지만, 대략적으로
- /srv/pypiserver/wsgi/
- pypiserver.wsgi
- /srv/pypiserver/auth/
- .htpasswd
- /srv/pypiserver/data/
- (여기에 패키지 파일이 업로드 된다)
- /srv/pypiserver/virtualenv/
네 개의 디렉토리로 나뉘게 된다. wsgi 와 auth, data와 파이썬 실행 환경이 있는 virtualenv (!=venv) 는 서로 섞지 않는 원칙을 정했다. 적어도 상대경로 참조를 통한 구닥다리 공격은 방지되었을 것이라는 가정이다.
Apache2 설정
/etc/apache2/sites-enabled/default-ssl.conf 을 수정할 것이다.
WSGIScriptAlias / /srv/pypiserver/wsgi/pypiserver.wsgi
WSGIDaemonProcess pypisrv user=pypisrv group=pypisrv umask=0007 \
processes=1 threads=5 maximum-requests=500 \
display-name=wsgi-pypisrv inactivity-timeout=300 \
python-home=/srv/pypiserver/virtualenv
<Directory /srv/pypiserver/wsgi >
Require all granted
WSGIProcessGroup pypisrv
WSGIApplicationGroup %{GLOBAL}
# Required for authentication (https://github.com/pypiserver/pypiserver/issues/288)
WSGIPassAuthorization On
</Directory>
의 내용을 <VirtualHost *:443> ... </VirtualHost>에 끼워넣었다. wsgi에서 프로세스를 실행시키는 사용자와 그룹은 pypisrv:pypisrv로 정했다. (adduser 명령어 참조)
wsgi 설정
사실 이 포스팅을 하게 된 이유이기도 하다. 공식 문서 (https://github.com/pypiserver/pypiserver) 에는
import pypiserver
conf = pypiserver.default_config(
root = "/yoursite/packages",
password_file = "/yoursite/htpasswd", )
application = pypiserver.app(**conf)
로 되어 있으나, pypiserver 2.0.1 버전에서는 default_config 가 존재하지 않는다. 문서가 업데이트가 되지 않은 것이다. ㅠㅠ
pypiserver.wsgi 내용
import pypiserver
application = pypiserver.app(roots=["/srv/pypiserver/data"], authenticate=["update", "download", "list"], password_file="/srv/pypiserver/auth/.htpasswd", overwrite=False)
- authenticate의 기본값은 업로드 시에만 암호를 물어보는 ["update"] 이나, 여기서는 비공개 패키지이므로 ["update", "download", "list"] 까지 해서 조회, 다운로드, 업로드시 모두 암호를 물어보도록 설정했다.
- 한번 업로드한 패키지에 대해서는 덮어쓰는 것을 불가능하도록 하였다. 실제로 내용이 다른데, 같은 버전 넘버를 사용해서는 안된다고 생각한다.
- 다음으로 "roots" 인데, 여기에 str의 배열을 사용하지 않고 roots="/srv/pypiserver/data"를 입력하게 되면 파일 시스템 전체를 훑게 된다. 따라서 503 Internal Server Error가 발생하게 되고, 아파치 로그를 살펴보다보면 엉뚱한 파일들을 접근하고 있는 것을 보게 된다.
return self._accessor.stat(self, follow_symlinks=follow_symlinks)
[Sun Feb 25 20:56:38.478903 2024] [wsgi:error] [pid 2407370:tid 140154756912704] [remote ***] PermissionError: [Errno 13] Permission denied: '/opt/gitlab/embedded/service/gitlab-rails/config/redis.yml'
왜 이런 현상이 발생했을까? "/srv/pypiserver/data" 를 ["/", "s", "r", "v", ..., "d", "a", "t", "a"] 처럼 쪼개버려 첫 "/" 에서 전체 파일 시스템을 읽어버리게 되는 것이다. roots=["/srv/pypiserver/data"] 와 같이 str 배열을 사용해야 의도한대로 동작한다. 이건 파이썬에서 duck typing이 일상이기 때문에 발생하는 문제다.