파일 서버를 구축했으니 홈 페이지를 만들어보자. 홈 페이지는 개인 개발 관련 포스트만 게시할 예정이기 때문에 구성이 어렵지 않은 정적 웹 페이지 생성기를 활용할 것이다.

Hugo는 Go로 작성된 오픈소스 SSG(Static Site Generator)로, Go로 작성되어 사이트 생성 속도가 매우 빠르다. 개인적으로 애용하는 Markdown 형식 문서를 레이아웃에 맞추어 쉽게 웹 페이지로 만들어주기 때문에 마음에 든다.

1. Hugo

설치와 최초 설정

우선 서버에 Hugo를 설치한다. 패키지 매니저에서도 구할 수 있기는 한데, 각종 추가 기능을 제공하는 Extended 버전이 아닐 가능성이 높기 때문에 Linux에서 설치하는 경우 대부분 공식 GitHub 저장소의 Release 페이지에서 최신 Extended 버전을 설치하는 것을 권장한다. Hugo의 테마 대부분이 Extended 기능을 기대하기 때문에 이 부분은 어쩔 수 없는 것 같다. Hugo 설치 후 아래 명령어를 통해 Extended 버전이 잘 설치되었는지 확인한다.

hugo version

이제 원하는 지점에 Hugo 관련 작업을 수행할 디렉토리를 생성한다. 서버에 SSH로 작업할 수도 있긴 하지만 그냥 편하게 작업은 다른 위치에서 하고 사이트 생성 및 배포만 서버에서 처리하기로 한다.

mkdir -p <사이트를 생성할 위치>/hugo/site
cd <사이트를 생성할 위치>/hugo/site
hugo new site <name>

사이트를 처음 생성하면 확인할 수 있는 파일 구조는 다음과 같다.

<name>/
├─ archetypes/
├─ content/
├─ layouts/
├─ static/
├─ themes/
├─ ...
└─ hugo.toml
  • archetypes: Hugo는 포스트의 메타데이터(제목, 생성 시간 등)를 문서 최상단의 YAML 형식 Formatter를 통해 처리하는데, 포스트를 새로 생성할 때 자동으로 붙여주는 Formatter의 템플릿을 저장하는 공간이다. default.md는 기본적으로 모든 포스트에 적용되며, posts.md같은 이름의 파일을 추가로 저장하면 posts 아래에 생성되는 포스트들만 해당 파일이 적용된다.

  • content: 실제 문서들이 생성되는 공간이다. 대부분의 테마들이 content/posts 아래에 실제 포스트가 작성되기를 기대한다. 물론 content 아래라면 어느 곳이든 문서를 생성할 수 있는데, content/about과 같은 곳에 포스트를 생성하면 <사이트 주소>/about에서 해당 문서를 접근할 수 있는 방식이다.

  • layouts: 사이트에 적용될 레이아웃을 설정하는 공간이다. 웹 개발 쪽은 문외한이라 관련 부분은 모두 테마에 맡기지만, 테마에서 일부 수정하고 싶은 곳이 있으면 이 곳에 별도로 정의하기도 한다.

  • static: favicon이나 CSS 파일처럼 별도의 정적 파일을 보관하는 공간이다.

  • hugo.toml: Hugo의 핵심이 되는 파일로, 사이트 생성에 필요한 모든 설정을 이 곳에서 수행한다. config.toml도 동일한 역할을 한다.

사이트를 생성한 후 우선 테마를 설정해보자. Hugo Themes에서 원하는 테마의 Git 저장소 위치를 확인하여 사이트 디렉토리에 서브모듈로 추가한다. 이번 프로젝트에서는 hello-friend-ng 테마를 사용할 것이다.

git init
git submodule add https://github.com/rhazdon/hugo-theme-hello-friend-ng.git themes/hello-friend-ng

테마에 맞는 설정은 hugo.toml에서 진행한다. 이 부분은 테마마다, 취향마다 알아서 설정하면 되는 부분이기 때문에 자세하게 다루지는 않는다.

콘텐츠 작성

글은 그냥 hugo new <글 위치>로 생성해서 작업하면 되지만, 개인적으로 몇번 써보면서 편했던 설정 몇 가지를 소개한다.

  1. Formatter
---
title: ""
date: {{ .Date }}
lastmod: {{ .Date }}
draft: true

categories: []
tags: []
series: []

description: ""

toc: true
---

현재 사이트에 글을 작성할 때 사용중인 Formatter다. draft는 초안 기능인데, true로 되어있으면 실제 페이지 배포에는 포함되지 않는다(-D 옵션을 통해 포함 가능). categories, tag, series는 글을 분류하는 방법으로, 실제 디렉토리 구조와는 별도의 Taxonomy로 처리된다.

toc는 대부분의 테마가 지원하지만 테마에 따라 지원 여부가 다른 것으로 알고있다. Table of Contents의 약자로, 글의 목차를 자동으로 생성하여 보여주는 기능이다.

위 내용을 default.mdposts.mdarchetypes 아래에 저장하면 글을 생성할 때 자동으로 글 최상단에 붙여준다.

  1. 디렉토리 구조

content 아래에 Markdown이나 HTML 파일을 추가하면 자동으로 글이 사이트에 포함되지만, 관리를 쉽게하기 위해서 추천하는 디렉토리 구조가 있다.

Hugo는 first.md 같이 하나의 파일만 저장해두어도 하나의 글로 인식하지만, 개인적으로 first같은 디렉토리를 생성하고 그 아래에 index.md로 글을 작성하기를 추천한다. 하나의 글을 별도의 파일로 구성하면 이미지와 같은 정적 파일을 글에 추가하고 싶을 때 해당 정적 파일의 저장 위치가 상당히 애매해지기 때문. first 디렉토리 아래 index.md와 같은 위치에 정적 파일을 저장해두면 글에서도 []()를 통해 쉽게 접근할 수 있고, 다른 글에 링크를 걸 때도 ../second와 같이 쉽게 링크를 만들 수 있다.

사이트 내 다른 글에 링크를 걸 때 유의할 점이 있는데, 우리가 일반적인 Markdown을 작성하듯이 [second](./second.md)로 작성해버리면 실제 사이트에서는 URL이 이상해져 링크가 제대로 걸리지 않는다. 이 때문에 위와 같은 구조를 더 추천하는 것이다.

디렉토리 구조와 관련하여 하나 더 알아두면 좋은 사실은 섹션(글 목록)과 포스트의 구분 방식이다. Hugo는 content 아래의 디렉토리를 자동으로 섹션으로 구분하지만, 그 외 사용자가 별도로 섹션 구조를 만들고 싶다면 해당 디렉토리 아래에 _index.md 파일을 만들어 주면 된다. _index.md가 인식되면 Hugo는 해당 디렉토리를 포스트에 대한 디렉토리가 아니라 섹션, 즉 글 목록을 나타내는 디렉토리로 이해한다. 파일 내용은 간단하게 아래와 같이 만들면 된다.

---
title: "<섹션 제목>"
---

여기에 글을 추가하면 섹션에서도 해당 글을 확인할 수 있다.

반대로 content 아래의 디렉토리를 섹션이 아니라 포스트로 인식시키려면 해당 디렉토리에 index.md를 명시적으로 생성해주면 된다.

테스트

사이트를 어느 정도 구성했다면 실제 서비스 전에 Hugo만으로 테스트를 해볼 수 있다. 아래 명령어를 입력한다.

hugo server

-D 옵션을 추가하면 초안들도 모두 포함하여 확인할 수 있다. 이후 http://localhost:1313으로 접속하여 사이트의 모습을 확인할 수 있다.

배포

Hugo는 정적 사이트 생성기일 뿐, 실제 서비스까지 진행하지는 않는다(hugo server는 사실상 테스트 용도). 우리는 Hugo로 빌드한 사이트를 nginx로 서비스한다.

홈 서버로 접속하여 방금 작성한 Hugo 파일들을 받은 후, 아래 명령어를 통해 빌드한다.

hugo --minify -d <배포 파일 위치>

--minify는 사이트를 구성하는 파일들을 압축(쓸데없는 부분 제거)하여 빌드하기 때문에 사이트의 기능은 유지하되 그 크기만 줄인다. 과거에는 이 옵션 때문에 사이트가 깨지는 경우도 있었다고 하나, 지금은 대부분 문제없이 동작한다고 한다.

-d로 배포 파일 위치를 따로 지정하지 않으면 동일한 디렉토리 내 public으로 빌드되는데, 내 경우와 같이 별도로 작업 후 서버에 올려 빌드를 진행하는 경우라면 이미 배포가 진행중인 디렉토리를 건드는 것이 좋은 구조는 아니므로, 별도로 배포 디렉토리를 만들어 해당 위치에서 배포를 진행한다. hugo/sites/<사이트 이름>/에서 빌드를 진행하고, 그 결과는 hugo/deploy/<사이트 이름>/에서 배포하는 것이 적당한 구조다.

ACL 설정

이 부분은 OMV에서 nginx HTTPS를 서비스할 때 확인해야 할 부분이다. 원래는 배포할 사이트 정적 파일에 www-data 유저가 접근하는 것이 문제가 없는데, 내 경우에는 OS 디스크(microSD)가 아니라 공유 폴더로 사용할 디스크(NVME)에 정적 파일들을 저장하여 배포할 예정이기 때문에 권한 부여가 조금 애매해진다. namei -mo를 통해 배포 위치의 권한을 확인하면 아래와 같다.

drwxr-xr-x root   root   /
drwxr-xr-x root   root   srv
drwxrws--- root   users  dev-disk-by-uuid-<디스크 UUID>
drwxrws--- <사용자> <사용자> hugo
drwxrws--- <사용자> <사용자> deploy
drwxrws--- <사용자> <사용자> <사이트 이름>

배포 파일이 서버에 접속한 유저 소유로 되어있고, others에게는 아무 권한이 없기 때문에 nginx HTTPS 서비스를 위한 www-data 유저의 접근이 불가능하다. 이 상태로 배포를 진행하면 사이트 주소로 접속해도 403 Forbidden만 뜰 것이다.

제일 간단한 방법은 www-data 유저를 users 그룹에 넣고 hugo 아래 디렉토리를 others에게 공개하는 것이다. 하지만 이는 보안적으로 매우 좋지 못한 방법인데, 공유 폴더로 사용하기 위해 마운트된 디스크를 의도적으로 막아두었음에도 www-data가 외부 접속을 통해 마음대로 디스크를 헤집고 다닐 수 있도록 만들어 버리는 것이기 때문.

일반적으로는 쉽게 접할 일이 없지만 Linux를 주로 사용하는 사용자들 입장에서 자주 접할 법한 ACL(Access Control List)이라는 물건이 있다. Linux의 기본 파일 접근 권한 외에 별도로 접근 권한을 조정할 수 있는 고급 도구인데, 꽤 복잡한 물건이라 자세한 설명은 넘어간다. 간단히 보자면 기능이 좀 애매한 Linux 기본 권한 관리에 특정 유저만 허락하는 등 추가적인 권한 부여 기능을 가진 것이라 보면 된다.

OMV 또한 기본적으로 ACL을 사용하는 패키지이기 때문에 ACL 자체는 이미 설치되어 있을 것이다. getfacl을 통해 특정 디렉토리 / 파일의 자세한 접근 권한을 확인할 수 있다. 현재 /srv/까지는 others가 통과할 수 있지만(x 권한), 그 아래부터는 others에 아무 권한이 없다. 다음 명령어를 통해 www-data에게 권한을 부여한다.

setfacl -m u:www-data:x /srv/dev-disk-by-uuid-<디스크 UUID>
setfacl -m u:www-data:x /srv/dev-disk-by-uuid-<디스크 UUID>/hugo
setfacl -m u:www-data:x /srv/dev-disk-by-uuid-<디스크-UUID>/hugo/deploy
setfacl -R -m u:www-data:rx /srv/dev-disk-by-uuid-<디스크-UUID>/hugo/deploy/<사이트 이름>

정적 사이트 디렉토리뿐 아니라 그 파일로 향하는 모든 부모 디렉토리에 최소한 통과 권한(x)은 있어야 함에 유의한다. 정적 사이트 디렉토리에서는 -R 옵션을 통해 하위 파일들에도 동일한 권한을 부여한다.

아래 명령어를 통해 www-data 유저에 대한 기본 ACL을 지정할 수 있으며, 이를 통해 새로 생성되는 파일에도 동일한 ACL을 적용할 수 있다. -d 옵션은 단순 기본값 지정 옵션이기 때문에 실제 파일에 ACL이 바로 적용되는 옵션은 아니라서, 지금과 같이 ACL이 필요한 파일에 우선 적용하고 기본 ACL을 지정한다.

setfacl -dR -m u:www-data:rx /srv/dev-disk-by-uuid-<디스크 UUID>/hugo/deploy/<사이트 이름>

2. nginx

이제 본격적으로 인터넷에 홈 페이지를 배포해보자. nginx는 OMV 웹 UI 또한 사용하고 있기 때문에 추가로 설치할 필요는 없다. 아래 명령어를 통해 실제 동작 중인지 확인한다.

systemctl status nginx

Linux에서 정적 웹 페이지를 서비스하기 위해서는 /etc/nginx/sites-available/<사이트 이름>.conf에 서비스 관련 설정을 정의하고 이를 /etc/nginx/sites-enabled/ 아래에 링크해두면 된다. 우선 설정 파일을 작성한다.

server {
    listen 80;
    server_name <도메인 이름> www.<도메인 이름>;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name <도메인 이름> www.<도메인 이름>;

    ssl_certificate /etc/letsencrypt/live/<도메인 이름>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<도메인 이름>/privkey.pem;

    root /srv/dev-disk-by-uuid-<디스크 UUID>/hugo/deploy/<사이트 이름>;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

SSL 인증을 사용하는 사이트의 기본적인 설정이다. 아래 명령어로 sites-enabled 아래에 링크를 생성한다.

sudo ln -s /etc/nginx/sites-available/<사이트 이름>.conf /etc/nginx/sites-enabled/

이제 테스트 후 nginx를 재실행하면 된다.

sudo nginx -t
sudo systemctl reload nginx

nginx -tsu 권한으로 실행하지 않으면 테스트에 실패하니 유의한다.

배포 자동화 쉘 스크립트

거창하게 자동화 스크립트라고 할 것 까진 없지만, 사이트에 새로운 글이 추가될 때마다 배포까지 진행되는 과정은 항상 동일하기 때문에 아래와 같이 쉘 스크립트를 구성해서 간단히 자동화할 수 있다.

#!/usr/bin/env bash
set -euo pipefail

SITE_DIR=<Hugo 디렉토리>
OUT_DIR=<배포할 정적 사이트 디렉토리>

cd "$SITE_DIR"

hugo --minify -d "$OUT_DIR"

sudo nginx -t
sudo systemctl reload nginx

Reverse Proxy

이제 서버에서 서로 다른 포트로 서비스되는 웹 사이트가 두 개(OMV 웹 UI, 홈 페이지)가 되었다. 이런 경우 외부에는 동일하게 443 포트만 공개하고 내부에서 서브 도메인으로 구분하는 방법이 있는데, 이를 Reverse Proxy라 한다.

OMV 웹 UI를 omv 서브 도메인에 구성한다고 가정하면, nginx 설정 파일에 아래와 같이 추가하여 Reverse Proxy를 구성할 수 있다.

server {
    listen 80;
    server_name omv.<도메인 이름>;
    return 301 https://$uri$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name omv.<도메인 이름>;

    ssl_certificate /etc/letsencrypt/live/<도메인 이름>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<도메인 이름>/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:<OMV 웹 UI 포트>;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }
}

참고로 이 때 OMV 웹 UI 자체는 HTTPS를 사용하지 않는데, SSL은 Reverse Proxy 단에서 인증하고 이후 접속은 서버 내에서 이루어지기 때문에 반드시 보안 연결을 유지할 필요는 없다. 이 때 OMV Workbench 설정에서 강제 SSL 사용 옵션이 켜져있으면 문제가 생길 수 있기 때문에 해당 옵션은 꺼두는 것이 좋다.

Basic Auth

OMV 웹 UI 자체에 이미 비밀번호를 통한 인증이 적용되어 있긴 하지만, OMV 웹 UI 자체를 인터넷에 공개해두는 것은 보안적으로 좋지 않다. 아무리 집 현관이 도어락으로 잠겨있다고 해도, 도어락 자체를 사람들이 누구든 접근할 수 있는 공간에 두면 좋지 않은 것과 같은 의미다. 모르는 사람이 집 현관 도어락을 무차별 대입 공격하고 있다면 얼마나 꺼림칙하겠는가?

너무 과하지 않은 선에서 인증 기능을 추가하는 법이 있는데, nginx단에서 Basic Auth를 추가하는 것이다. 대부분의 상용 서비스들은 보안성이 더 좋은 OAuth와 같은 인증을 사용하지만, 간단히 인증 기능을 추가하고 싶을 때는 비밀번호 기반의 Basic Auth 또한 여전히 좋은 방법이다.

이 Basic Auth를 사용할 때 중요한 점은 반드시 HTTPS 프로토콜과 함께 사용되어야 한다는 것이다. 통신 구간이 암호화되어 있지 않으면 인증 과정에서 해싱된 비밀번호가 평문으로 공개되기 때문에 Base64 등으로 바로 해석이 가능하다. 중간자 공격(MITM)에 심각하게 취약해지는 것이다.

Basic Auth를 위한 비밀번호 파일은 htpasswd를 통해 생성할 수 있다. htpasswd는 Debian에서 apache2-utils를 통해 사용할 수 있다.

sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd <사용자 ID>

비밀번호를 입력하면 .htpasswd 파일이 생성된다. nginx 설정 파일에서 location단 위에 아래 내용을 추가하여 설정한다.

auth_basic "<설명 문자열>";
auth_basic_user_file /etc/nginx/.htpasswd;

이제 웹 브라우저에서 omv.<도메인 이름>으로 접속 시도 시 사용자 ID와 비밀번호를 묻는 창이 뜬다.

0.png

이 기능을 조금 응용하면 홈 페이지 내에서도 특정 메뉴를 인증된 사용자만 접근하도록 할 수 있다.


이것으로 프로젝트를 시작할 때 목표로 했던 바는 모두 달성했다. 여기저기서 삽질하면서 예상보다 시간이 매우 많이 걸렸지만, 그만한 가치가 있었던 프로젝트였던 것 같다.