Hugo on GitLab Page

注意:本篇是使用Hogo作為示範

Prepare

  1. 請至GitLab User Settings > Access Tokens
    產生一個具有api scope權限的token(含read_write_repository)

  2. 請至GitLab Project > Settings > CI/CD > Variables

    • add GITLAB_API_TOKEN變數,值為剛剛1.產生的 Access Tokens
    • add RENEW_DAYS_THRESHOLD變數,值為30

流程圖

  1. 建立第一個gitlab pipeline job, 取得/計算SSL憑證剩餘天數
  2. 判斷剩餘天數,若 < 30時,則會進行SSL憑證更新動作
  3. certbot : 使用 http challenges 方法
  4. 觸發 auth.sh
    • 取得 $CERTBOT_VALIDATION(驗證內容), $CERTBOT_TOKEN(檔名) 變數
    • git commit push static/.well-known/acme-challenge/$CERTBOT_TOKEN → 然後此處會觸發第二個gitlab pipeline進行 gitlab page更新
    • wait for .well-known page to become successful (200)
  5. 跟Let’s Encrypt說已經有/.well-known/acme-challenge/$CERTBOT_TOKEN這個頁面,請他來確認
  6. 回傳 fullchain.pem, privkey.pem參考
    • 更新gitlab page domain settings 的 SSL private和public key
  7. 觸發 cleanup.sh, 將 .well-known/ page 刪除 (commit update → trigger pipeline → gitlab page update)

Step1 : Add letsencrypt renew job to yaml

.gitlab-ci.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
letsencrypt-renew:
  image: scottchayaa/alpine-certbot:3.7
  variables:
    GITLAB_API_TOKEN: $GITLAB_API_TOKEN
    RENEW_DAYS_THRESHOLD: $RENEW_DAYS_THRESHOLD
    DOMAIN: "yourdomain.com"
  script:
    - git config --global user.name $GITLAB_USER_LOGIN
    - git config --global user.email $GITLAB_USER_EMAIL
    - chmod +x ./letsencrypt/*.sh
    - ./letsencrypt/renew.sh
  only: 
    - schedules
  • 1.1. 參考別人寫的文章後,因image速度需求,自己改寫成alpine版本image : scottchayaa/alpine-certbot
  • 1.2. 只允許schedules執行此job → 後面會提到如何在GitLab設定Schedules
  • 1.3. git config 設定user資訊 → 後面auth.sh、cleanup.sh會使用到
  • 1.4. alpine:3.7的certbot version為0.19版,功能正常。但如果要在alpine:3.8安裝certbot,則會出現以下錯誤:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    Traceback (most recent call last):
    File "/usr/bin/certbot", line 6, in <module>
    from pkg_resources import load_entry_point
    File "/usr/lib/python2.7/site-packages/pkg_resources/__init__.py", line 3086, in <module>
    @_call_aside
    File "/usr/lib/python2.7/site-packages/pkg_resources/__init__.py", line 3070, in _call_aside
    f(*args, **kwargs)
    File "/usr/lib/python2.7/site-packages/pkg_resources/__init__.py", line 3099, in _initialize_master_working_set
    working_set = WorkingSet._build_master()
    File "/usr/lib/python2.7/site-packages/pkg_resources/__init__.py", line 576, in _build_master
    return cls._build_from_requirements(__requires__)
    File "/usr/lib/python2.7/site-packages/pkg_resources/__init__.py", line 589, in _build_from_requirements
    dists = ws.resolve(reqs, Environment())
    File "/usr/lib/python2.7/site-packages/pkg_resources/__init__.py", line 783, in resolve
    raise VersionConflict(dist, req).with_context(dependent_req)
    pkg_resources.ContextualVersionConflict: (idna 2.7 (/usr/lib/python2.7/site-packages), Requirement.parse('idna<2.7,>=2.5'), set(['requests']))

目前官方還沒有修正此問題,個人研判是build certbot時使用的python版本或套件出了問題,如果有修正再回來更新此問題

Step2 : Run certbot script

letsencrypt/renew.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/sh
end_epoch=$(date -d "$(echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | openssl x509 -enddate -noout | cut -d'=' -f2)" "+%s")
current_epoch=$(date "+%s")
days_diff=$((($end_epoch - $current_epoch) / 60 / 60 / 24))
if [ $days_diff -lt $RENEW_DAYS_THRESHOLD ]; then
    echo "============================"
    echo "Certificate is $days_diff days old, renewing now."
    echo "============================"
    certbot certonly \
    --preferred-challenges http \
    --manual \
    --agree-tos \
    --eff-email \
    -m "$GITLAB_USER_EMAIL" \
    -d "$DOMAIN" \
    --manual-public-ip-logging-ok \
    --manual-auth-hook ./letsencrypt/auth.sh \
    --manual-cleanup-hook ./letsencrypt/cleanup.sh
    echo "============================"
    echo "Certbot finished. Updating GitLab Pages domains."
    echo "============================"
    curl --request PUT --header "PRIVATE-TOKEN: $GITLAB_API_TOKE" --form "certificate=@/etc/letsencrypt/live/$DOMAIN/fullchain.pem" --form "key=@/etc/letsencrypt/live/$DOMAIN/privkey.pem" https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/pages/domains/$DOMAIN
else
    echo "============================"
    echo "Certificate still valid for $days_diff days, no renewal required."
    echo "============================"
fi
  • 2.1. days_diff : 運算$DOMAIN SSL的剩餘時間,與$RENEW_DAYS_THRESHOLD比較,若小於30天擇進行renew ssl
  • 2.2. certbot 指令請參考 : Certbot command-line options
  • 2.3. preferred-challenges使用http方式驗證,會詢問/.well-known/acme-challenge/裡面有沒有指定的CERTBOT_TOKEN檔案
  • 2.4. --manual-auth-hook : 驗證測試前的事件;--manual-auth-hook : 驗證測試後的事件
  • 2.5. Certbot指令完成後,會在/etc/letsencrypt/live/$DOMAIN產生fullchain.pemprivkey.pem,我們透過GitLab API方式Update pages domain
  • 2.6. certbot hooks流程圖 :

letsencrypt/auth.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh
echo -e "---\npermalink: /.well-known/acme-challenge/$CERTBOT_TOKEN/\n---\n$CERTBOT_VALIDATION" > ./ssl.html
git add ./ssl.html
git commit -m "GitLab runner - Add certbot challenge file for certificate renew"
git push https://$GITLAB_USER_LOGIN:$GITLAB_API_TOKE@gitlab.com/$CI_PROJECT_PATH.git HEAD:master
interval_sec=10
max_tries=30
n_tries=0
while [ $n_tries -le $max_tries ]
do
  status_code=$(curl -s -o /dev/null -I -w "%{http_code}" https://$DOMAIN/.well-known/acme-challenge/$CERTBOT_TOKEN/)
  if [[ $status_code -eq 200 ]]; then
    echo $status_code
    exit 0
  fi
  n_tries=$((n_tries+1))
  sleep $interval_sec
done
exit 1 
  • 2.7. 因為是使用jekyll,所以我們產生ssl.html,裡面塞permalink導向/.well-known/acme-challenge/$CERTBOT_TOKEN
  • 2.8. git add ssl.html 更新gitlab page → 至少要有2個以上的gitlab-runner
  • 2.9. 注意curl驗證200的連結務必最後要加’/‘,否則如果只有.../$CERTBOT_TOKEN則會得到return status code 302

letsencrypt/cleanup.sh

1
2
3
4
5
#!/bin/sh
git rm ./ssl.html
git commit -m "GitLab runner - Removed certbot challenge file"
git push https://$GITLAB_USER_LOGIN:$GITLAB_API_TOKE@gitlab.com/$CI_PROJECT_PATH.git HEAD:master
exit 0
  • 2.10. git rm ssl.html 更新gitlab page → certbot驗證完成後刪除ssl.html

Step3 : Set gitlab-ci schedule for the letsencrypt-renew job

GitLab Project > CI/CD > Schedules > New schedule

其他

1
2
3
4
5
6
7
8
# 顯示SSL開始/結束時間
$ echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | openssl x509 -noout -dates
notBefore=Mar 31 00:38:23 2019 GMT
notAfter=Jun 29 00:38:23 2019 GMT

# 顯示SSL結束時間
$ echo | openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | openssl x509 -noout -enddate
notAfter=Jun 29 00:38:23 2019 GMT

後記

後來 Gitlab 在 2019.07.22 發佈 12.1 版更新

Get automatic HTTPS certs for Pages using Let’s Encrypt

官方已經在 GitLab pages 增加了 Let’s Encrypt SSL 自動更新功能
操作步驟很簡單 :

  1. Settings > Pages
  2. 點選 Details
  3. 點選 Edit
  4. 打開開關 Automatic certificate management using Let's Encrypt:

PS : 所以我之前做的那些都是多餘的 XD…..

References