外部公開していないオンプレのGitLabとRedmineをLet's Encryptの証明書で暗号化する

このサイトでよくGitLabやRedmine関連の構築を書いていますが、
関連するツールはどれもHTTPSの暗号化通信前提の作りになっているんですよね。
そこで自己証明書でHTTPS化しているのですが、以下のエラーに毎度悩まされます。

x509: certificate signed by unknown authority

と言うわけでLet’s Encrypt証明書に変更して
自己証明書を卒業したいと思います!

でも検証目的のサーバーなので外部公開したくない。
なのでHTTP通信が不要なDNS認証を利用して証明書を発行します。

必要なもの

  • ドメイン
    お名前.comとかで買えるやつです。
    本ブログで言うとtsuchinokometal.comですね。
  • DNSサーバー
    自動更新したいならAPIが使えるAWSやGCPがおすすめですね。
    CertbotのDNSプラグインが使えるとさらに楽です。
    今回は検証環境を想定して自動更新については書きません.
  • サーバーとインターネット環境

ドメインやDNSサーバーは無料のサービスもあるようなので、
それらを使えばお金を使わずに構築できるかも?

構成

以前 GitLabとRedmineの連携で、
自己証明書でのHTTPSをNGINXのリバースプロキシで受ける構成を書いたので、
今回はその自己証明書を入れ替えようと思います。

前回と同じくOSはCentOS7です。

[centos@example ~]$ cat /etc/redhat-release 
CentOS Linux release 7.9.2009 (Core)

Certbotのインストール

CertbotはLet’s Encrypt証明書を取得するためのツールです。
まずはこちら を参考にCertbotをインストールしてください。

ご自身の環境に合わせて選択してください。

certbot_gitlab_redmine_01.png

snapを使ったインストールを推奨しているようなので こちら を参考にまずsnapをインストールします。
UbuntuにaptでCertbotをインストールしたらDNSプラグインがインストールできなかったので
snapを使った方がいいと思います。

[centos@example ~]$ sudo yum install epel-release
[centos@example ~]$ sudo yum install snapd
[centos@example ~]$ sudo systemctl enable --now snapd.socket
Created symlink from /etc/systemd/system/sockets.target.wants/snapd.socket to /usr/lib/systemd/system/snapd.socket.
[centos@example ~]$ sudo ln -s /var/lib/snapd/snap /snap

Certbotのインストールに戻ります。
僕の場合以下のエラーが発生しましたが、一旦ログインし直すと直りました。

[centos@example ~]$ sudo snap install core; sudo snap refresh core
error: too early for operation, device not yet seeded or device model not acknowledged
error: too early for operation, device not yet seeded or device model not acknowledged
[centos@example ~]$ exit

インストールできました。

[centos@example ~]$ sudo snap install core; sudo snap refresh core
2021-05-25T18:49:49+09:00 INFO Waiting for automatic snapd restart...
core 16-2.50 from Canonical✓ installed
snap "core" has no updates available
[centos@example ~]$ sudo snap install --classic certbot
certbot 1.15.0 from Certbot Project (certbot-eff✓) installed

手動で証明書を発行する

Certbotの使い方は詳しく説明しているサイトがたくさんあるので省略します。
僕の場合は以下のコマンドで実行します。

[root@example ~]# certbot certonly --manual -d example.tsuchinokometal.com -d *.example.tsuchinokometal.com --preferred-challenges dns -m [あなたのメールアドレス] --agree-tos --no-eff-email --manual-public-ip-logging-ok
Use of --manual-public-ip-logging-ok is deprecated.
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Account registered.
Requesting a certificate for example.tsuchinokometal.com and *.example.tsuchinokometal.com
Performing the following challenges:
dns-01 challenge for example.tsuchinokometal.com
dns-01 challenge for example.tsuchinokometal.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name:

_acme-challenge.example.tsuchinokometal.com.

with the following value:

J_pPdq-P-nWuP7w1caChI-fWwM1bu_zHZcx6m0Bqp60

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name:

_acme-challenge.example.tsuchinokometal.com.

with the following value:

-feqwo9Ob3zAKQnfDG56GvgRbcaFgwadejin_JNXRxA

(This must be set up in addition to the previous challenges; do not remove,
replace, or undo the previous challenge tasks yet. Note that you might be
asked to create multiple distinct TXT records with the same name. This is
permitted by DNS standards.)

Before continuing, verify the TXT record has been deployed. Depending on the DNS 
provider, this may take some time, from a few seconds to multiple minutes. You can
check if it has finished deploying with aid of online tools, such as the Google
Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.example.tsuchinokometal.com.
Look for one or more bolded line(s) below the line ';ANSWER'. It should show the
value(s) you've just added.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

ここまできたらエンターは押さずにそのままで、
指定されたホスト名と値でDNSサーバーにTXTレコードで設定します。
(ドメインを2つ指定しているので2レコード設定が必要です)

設定の仕方はDNSサーバーによって違いますが、
digコマンドで以下のように返って来るようになれば大丈夫です。

% dig -n txt _acme-challenge.example.tsuchinokometal.com +short
"-feqwo9Ob3zAKQnfDG56GvgRbcaFgwadejin_JNXRxA"
"J_pPdq-P-nWuP7w1caChI-fWwM1bu_zHZcx6m0Bqp60"

ではエンターを押して進んでください。
以下のようになれば証明書発行完了です。

Waiting for verification...
Cleaning up challenges
Use of --manual-public-ip-logging-ok is deprecated.

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/example.tsuchinokometal.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/example.tsuchinokometal.com/privkey.pem
   Your certificate will expire on 2021-08-25. To obtain a new or
   tweaked version of this certificate in the future, simply run
   certbot again. To non-interactively renew *all* of your
   certificates, run "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

書いてあるとおり、/etc/letsencrypt/live/[あなたのドメイン]/に、
発行された証明書などが保存されていると思います。

もう必要ないのでTXTレコードは削除してしまっていいです。
これだけで無料で証明書が発行できるなんて! ありがとうLet’s Encrypt!

発行した証明書を利用する

GitLabやRedmineで使ってみます。
例えば以下のようなディレクトリ構成にしたとします。

.
├── gitlab
│   └── docker-compose.yml
├── proxy
│   ├── docker-compose.yml
│   └── ssl.conf
└── redmine
    └── docker-compose.yml

まずGitLabとRedmineを起動しておいてください。
以前書いたこちら が参考になると思います。

今回使ったdocker-compose.ymlをそのまま載せます。
外部ネットワークを作成して全てのコンテナが
同じネットワークに作成されるようにしてください。

gitlab/docker-compose.yml

version: '2.3'

services:
  redis:
    restart: always
    image: redis:5.0.9
    container_name: redis
    command:
    - --loglevel warning
    volumes:
    - redis-data:/var/lib/redis:Z

  postgresql:
    restart: always
    image: sameersbn/postgresql:12-20200524
    container_name: postgresql
    volumes:
    - postgresql-data:/var/lib/postgresql:Z
    environment:
    - DB_USER=gitlab
    - DB_PASS=password
    - DB_NAME=gitlabhq_production
    - DB_EXTENSION=pg_trgm,btree_gist

  gitlab:
    restart: always
    image: sameersbn/gitlab:13.12.0
    container_name: gitlab
    depends_on:
    - redis
    - postgresql
    ports:
    - "10080:80"
    - "10022:22"
    volumes:
    - gitlab-data:/home/git/data:Z
    - /etc/letsencrypt:/letsencrypt:ro
    healthcheck:
      test: ["CMD", "/usr/local/sbin/healthcheck"]
      interval: 5m
      timeout: 10s
      retries: 3
      start_period: 5m
    environment:
    - DEBUG=false

    - DB_ADAPTER=postgresql
    - DB_HOST=postgresql
    - DB_PORT=5432
    - DB_USER=gitlab
    - DB_PASS=password
    - DB_NAME=gitlabhq_production

    - REDIS_HOST=redis
    - REDIS_PORT=6379

    - TZ=Asia/Tokyo
    - GITLAB_TIMEZONE=Tokyo

    - GITLAB_HTTPS=true
    - SSL_SELF_SIGNED=false

    - GITLAB_HOST=example.tsuchinokometal.com
    - GITLAB_PORT=443
    - GITLAB_SSH_PORT=10022
    - GITLAB_RELATIVE_URL_ROOT=/gitlab
    - GITLAB_SECRETS_DB_KEY_BASE=long-and-random-alphanumeric-string
    - GITLAB_SECRETS_SECRET_KEY_BASE=long-and-random-alphanumeric-string
    - GITLAB_SECRETS_OTP_KEY_BASE=long-and-random-alphanumeric-string

    - GITLAB_ROOT_PASSWORD=
    - GITLAB_ROOT_EMAIL=

    - GITLAB_NOTIFY_ON_BROKEN_BUILDS=true
    - GITLAB_NOTIFY_PUSHER=false

    - GITLAB_EMAIL=notifications@example.com
    - GITLAB_EMAIL_REPLY_TO=noreply@example.com
    - GITLAB_INCOMING_EMAIL_ADDRESS=reply@example.com

    - GITLAB_BACKUP_SCHEDULE=daily
    - GITLAB_BACKUP_TIME=01:00

    - SMTP_ENABLED=false
    - SMTP_DOMAIN=www.example.com
    - SMTP_HOST=smtp.gmail.com
    - SMTP_PORT=587
    - SMTP_USER=mailer@example.com
    - SMTP_PASS=password
    - SMTP_STARTTLS=true
    - SMTP_AUTHENTICATION=login

    - IMAP_ENABLED=false
    - IMAP_HOST=imap.gmail.com
    - IMAP_PORT=993
    - IMAP_USER=mailer@example.com
    - IMAP_PASS=password
    - IMAP_SSL=true
    - IMAP_STARTTLS=false

    - OAUTH_ENABLED=false
    - OAUTH_AUTO_SIGN_IN_WITH_PROVIDER=
    - OAUTH_ALLOW_SSO=
    - OAUTH_BLOCK_AUTO_CREATED_USERS=true
    - OAUTH_AUTO_LINK_LDAP_USER=false
    - OAUTH_AUTO_LINK_SAML_USER=false
    - OAUTH_EXTERNAL_PROVIDERS=

    - OAUTH_CAS3_LABEL=cas3
    - OAUTH_CAS3_SERVER=
    - OAUTH_CAS3_DISABLE_SSL_VERIFICATION=false
    - OAUTH_CAS3_LOGIN_URL=/cas/login
    - OAUTH_CAS3_VALIDATE_URL=/cas/p3/serviceValidate
    - OAUTH_CAS3_LOGOUT_URL=/cas/logout

    - OAUTH_GOOGLE_API_KEY=
    - OAUTH_GOOGLE_APP_SECRET=
    - OAUTH_GOOGLE_RESTRICT_DOMAIN=

    - OAUTH_FACEBOOK_API_KEY=
    - OAUTH_FACEBOOK_APP_SECRET=

    - OAUTH_TWITTER_API_KEY=
    - OAUTH_TWITTER_APP_SECRET=

    - OAUTH_GITHUB_API_KEY=
    - OAUTH_GITHUB_APP_SECRET=
    - OAUTH_GITHUB_URL=
    - OAUTH_GITHUB_VERIFY_SSL=

    - OAUTH_GITLAB_API_KEY=
    - OAUTH_GITLAB_APP_SECRET=

    - OAUTH_BITBUCKET_API_KEY=
    - OAUTH_BITBUCKET_APP_SECRET=
    - OAUTH_BITBUCKET_URL=

    - OAUTH_SAML_ASSERTION_CONSUMER_SERVICE_URL=
    - OAUTH_SAML_IDP_CERT_FINGERPRINT=
    - OAUTH_SAML_IDP_SSO_TARGET_URL=
    - OAUTH_SAML_ISSUER=
    - OAUTH_SAML_LABEL="Our SAML Provider"
    - OAUTH_SAML_NAME_IDENTIFIER_FORMAT=urn:oasis:names:tc:SAML:2.0:nameid-format:transient
    - OAUTH_SAML_GROUPS_ATTRIBUTE=
    - OAUTH_SAML_EXTERNAL_GROUPS=
    - OAUTH_SAML_ATTRIBUTE_STATEMENTS_EMAIL=
    - OAUTH_SAML_ATTRIBUTE_STATEMENTS_NAME=
    - OAUTH_SAML_ATTRIBUTE_STATEMENTS_USERNAME=
    - OAUTH_SAML_ATTRIBUTE_STATEMENTS_FIRST_NAME=
    - OAUTH_SAML_ATTRIBUTE_STATEMENTS_LAST_NAME=

    - OAUTH_CROWD_SERVER_URL=
    - OAUTH_CROWD_APP_NAME=
    - OAUTH_CROWD_APP_PASSWORD=

    - OAUTH_AUTH0_CLIENT_ID=
    - OAUTH_AUTH0_CLIENT_SECRET=
    - OAUTH_AUTH0_DOMAIN=
    - OAUTH_AUTH0_SCOPE=

    - OAUTH_AZURE_API_KEY=
    - OAUTH_AZURE_API_SECRET=
    - OAUTH_AZURE_TENANT_ID=

    - GITLAB_PAGES_ENABLED=true
    - GITLAB_PAGES_HTTPS=true
    - GITLAB_PAGES_DOMAIN=example.tsuchinokometal.com
    - GITLAB_PAGES_PORT=443

    - GITLAB_REGISTRY_ENABLED=true
    - GITLAB_REGISTRY_HOST=example.tsuchinokometal.com
    - GITLAB_REGISTRY_PORT=443
    - GITLAB_REGISTRY_API_URL=http://registry:5000
    - GITLAB_REGISTRY_KEY_PATH=/letsencrypt/live/example.tsuchinokometal.com/privkey.pem

  registry:
    restart: always
    image: registry:2
    container_name: registry
    expose:
      - "5000"
    ports:
      - "5000:5000"
    volumes:
      - registry-data:/registry
      - /etc/letsencrypt:/letsencrypt:ro
    environment:
      - REGISTRY_LOG_LEVEL=info
      - REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/registry
      - REGISTRY_AUTH_TOKEN_REALM=https://example.tsuchinokometal.com/gitlab/jwt/auth
      - REGISTRY_AUTH_TOKEN_SERVICE=container_registry
      - REGISTRY_AUTH_TOKEN_ISSUER=gitlab-issuer
      - REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/letsencrypt/live/example.tsuchinokometal.com/fullchain.pem
      - REGISTRY_STORAGE_DELETE_ENABLED=true

volumes:
  redis-data:
  postgresql-data:
  gitlab-data:
  registry-data:

networks:
  default:
    external:
      name: tsuchinoko

GitLabからregistryにアクセスしようとしたら、
production.logで以下のエラーが出ました。

Completed 500 Internal Server Error in 42ms (ActiveRecord: 14.8ms | Elasticsearch: 0.0ms | Allocations: 17154)
Errno::EACCES (Permission denied @ rb_sysopen - /letsencrypt/live/example.tsuchinokometal.com/privkey.pem):

僕はアクセス権を書き換えて対応しましたが、もっといい方法があったら教えて欲しいです。

[root@example ~]# chmod -R 755 /etc/letsencrypt/archive/
[root@example ~]# chmod -R 755 /etc/letsencrypt/live/

GitLabとregistryの連携については こちら が参考になると思います。

redmine/docker-compose.yml

version: '2'

services:
  redmine_postgresql:
    restart: always
    image: sameersbn/postgresql:9.6-4
    container_name: redmine_postgresql
    environment:
    - DB_USER=redmine
    - DB_PASS=password
    - DB_NAME=redmine_production
    volumes:
    - redmine_postgresql-data:/var/lib/postgresql

  redmine:
    restart: always
    image: sameersbn/redmine:4.2.1
    container_name: redmine
    depends_on:
    - redmine_postgresql
    environment:
    - TZ=Asia/Tokyo

    - DB_ADAPTER=postgresql
    - DB_HOST=redmine_postgresql
    - DB_PORT=5432
    - DB_USER=redmine
    - DB_PASS=password
    - DB_NAME=redmine_production

    - REDMINE_PORT=10083
    - REDMINE_HTTPS=true
    - REDMINE_RELATIVE_URL_ROOT=/redmine
    - REDMINE_SECRET_TOKEN=

    - REDMINE_SUDO_MODE_ENABLED=false
    - REDMINE_SUDO_MODE_TIMEOUT=15

    - REDMINE_CONCURRENT_UPLOADS=2

    - REDMINE_BACKUP_SCHEDULE=
    - REDMINE_BACKUP_EXPIRY=
    - REDMINE_BACKUP_TIME=

    - SMTP_ENABLED=false
    - SMTP_METHOD=smtp
    - SMTP_DOMAIN=www.example.com
    - SMTP_HOST=smtp.gmail.com
    - SMTP_PORT=587
    - SMTP_USER=mailer@example.com
    - SMTP_PASS=password
    - SMTP_STARTTLS=true
    - SMTP_AUTHENTICATION=:login

    - IMAP_ENABLED=false
    - IMAP_HOST=imap.gmail.com
    - IMAP_PORT=993
    - IMAP_USER=mailer@example.com
    - IMAP_PASS=password
    - IMAP_SSL=true
    - IMAP_INTERVAL=30

    ports:
    - "10083:80"
    volumes:
    - redmine-data:/home/redmine/data
    - redmine-logs:/var/log/redmine

volumes:
  redmine_postgresql-data:
  redmine-data:
  redmine-logs:

networks:
  default:
    external:
      name: tsuchinoko

次にNGINXを起動します。

proxy/docker-compose.yml

version: '3'

services:
  proxy:
    image: nginx:stable
    container_name: proxy
    volumes:
      - ./ssl.conf:/etc/nginx/conf.d/ssl.conf
      - /etc/letsencrypt:/letsencrypt:ro
    ports:
      - "80:80"
      - "443:443"

networks:
  default:
    external:
      name: tsuchinoko

proxy/ssl.conf

server {
    root /dev/null;
    server_name *.example.tsuchinokometal.com example.tsuchinokometal.com;
    charset UTF-8;
    access_log /var/log/nginx/example.tsuchinokometal.com.access.log;
    error_log /var/log/nginx/example.tsuchinokometal.com.error.log;

    # Set up SSL only connections:
    listen *:443 ssl http2;
    ssl_certificate             /letsencrypt/live/example.tsuchinokometal.com/fullchain.pem;
    ssl_certificate_key         /letsencrypt/live/example.tsuchinokometal.com/privkey.pem;

    ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4';
    ssl_protocols  TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_session_cache  builtin:1000  shared:SSL:10m;
    ssl_session_timeout  5m;

    client_max_body_size        0;
    chunked_transfer_encoding   on;

    proxy_set_header  Host              $http_host;   # required for docker client's sake
    proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
    proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Proto $scheme;
    proxy_read_timeout                  900;
    
    location /gitlab {
        proxy_pass http://gitlab;
    }
    location / {
        proxy_pass http://gitlab;
    }
    location /redmine {
        proxy_pass http://redmine;
    }
    location /v2/ {
        proxy_pass http://registry:5000;
    }
    
}

server {
    listen *:80;
    server_name  *.example.tsuchinokometal.com example.tsuchinokometal.com;
    server_tokens off; ## Don't show the nginx version number, a security best practice
    return 301 https://$http_host:$request_uri;
}

では構築したサーバーの名前解決ができるように
内部DNSサーバーかhostsファイルへ設定を追加してください。
httpsでアクセスできるかご確認ください。

certbot_gitlab_redmine_02.png

registryと連携できていれば以下のページが表示できます。

certbot_gitlab_redmine_03.png

ログインしてみると例のエラーが表示されずにログインできますね!

% docker login example.tsuchinokometal.com 
Username: root
Password: 
Login Succeeded

さらにオンプレKubernetesと連携してGitLab Runnerをインストールしても、
例のエラーが表示されずにインストールできました!
こちら を参照してください。

さらにさらに、ワイルドカードでも証明書を作っておいたので、
GitLab Pagesも暗号化してアクセスできます!
こちら を参照してください。

certbot_gitlab_redmine_04.png

なんか集大成みたいな感じになりましたが、
GitLabはまだまだ便利な機能があるのでもっと使い倒して行きたいと思います。