CIサーバのセキュリティを向上する

前回の記事ではnginxによるSSLプロキシを有効にして通信路を暗号化しました。が、今のままでは平文のHTTPでもアクセスできる状態になっていますので、これを閉じます。

例によってdocker-copose.ymlです。

version: '2'

services:
  # リポジトリ
  gitbucket:
    image: takezoe/gitbucket
    container_name: gitbucket
    volumes:
      - ./gitbucket/data:/gitbucket
    environment:
      - TZ=JST-9
    ports:
      - 8080
      - 29418:29418

  # CIサーバ
  jenkins:
    image: jenkins:alpine
    container_name: jenkins
    volumes:
      - ./jenkins/home:/var/jenkins_home
      - ./jenkins/executors.groovy:/usr/share/jenkins/ref/init.groovy.d/executors.groovy
    environment:
      - JAVA_OPTS=-Duser.timezone=Asia/Tokyo
    ports:
      - 8080
      - 50000:50000

  # Rocket.Chatデータ
  db:
    image: mongo:3.0
    container_name: db
    volumes:
      - ./db/data:/data/db
    command: --smallfiles

  # Rocket.Chat本体
  rocket.chat:
    image: rocket.chat:latest
    container_name: rocket.chat
    environment:
      - ROOT_URL=http://localhost:8082
    links:
      - db
    ports:
      - 3000

  # リバースプロキシ
  nginx:
    image: nginx:alpine
    container_name: nginx
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/conf.d:/etc/nginx/conf.d
    depends_on:
      - gitbucket
      - jenkins
      - rocket.chat
    ports:
      # HTTPS
      - 8280:8280
      - 8281:8281
      - 8282:8282

フォントを赤くした3箇所のexposeを解除して、コンテナ間でのみ通信するように変更しました。Jenkinsの50000番ポートについてはJNLPによる接続なので、使わないようであればこれもexposeする必要はないと思います。

GitBucketの29418番ポートは外部とのSSH通信のためのポートですので、このままにしておきます。

nginxでリバースプロキシを立てる

さて、先の記事Let’s Encryptから取得したSSL証明書ですが、使わないと意味がありません。今回はnginxをHTTPSリバースプロキシとしてセットアップしてみます。

まず今回は既存のポートは生かしたまま、8280番ポートにGitBucket, 8281番ポートにJenkins, 8282番ポートにRocket.Chatを、それぞれ割り当てたいと思います。

例によってdocker-compose.ymlです。

version: '2'

services:
  # リポジトリ
  gitbucket:
    image: takezoe/gitbucket
    container_name: gitbucket
    volumes:
      - ./gitbucket/data:/gitbucket
    environment:
      - TZ=JST-9
    ports:
      - 8080:8080
      - 29418:29418

  # CIサーバ
  jenkins:
    image: jenkins:alpine
    container_name: jenkins
    volumes:
      - ./jenkins/home:/var/jenkins_home
      - ./jenkins/executors.groovy:/usr/share/jenkins/ref/init.groovy.d/executors.groovy
    environment:
      - JAVA_OPTS=-Duser.timezone=Asia/Tokyo
    ports:
      - 8081:8080
      - 50000:50000

  # Rocket.Chatデータ
  db:
    image: mongo:3.0
    container_name: db
    volumes:
      - ./db/data:/data/db
    command: --smallfiles

  # Rocket.Chat本体
  rocket.chat:
    image: rocket.chat:latest
    container_name: rocket.chat
    environment:
      - ROOT_URL=http://localhost:8082
    links:
      - db
    ports:
      - 8082:3000

  # リバースプロキシ
  nginx:
    image: nginx:alpine
    container_name: nginx
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/conf.d:/etc/nginx/conf.d
    depends_on:
      - gitbucket
      - jenkins
      - rocket.chat
    ports:
      # HTTPS
      - 8280:8280
      - 8281:8281
      - 8282:8282

既存の設定には手を入れず、末尾のnginxの設定のみを追加してあります。/etc/letsencryptに前回取得したSSL証明書がありますので、これをそのままボリュームとしてマウントします。その他、設定ファイルをいくつか上書きします。

まずはnginxの設定 nginx.conf です。通常、worker_processにはサーバのCPU数を指定します。

# nginx.conf
user  nginx;
worker_processes  2;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    #gzip  on;
    include /etc/nginx/conf.d/*.conf;
}

次の各サービスに対するプロキシの設定を、conf.dディレクトリ以下に gitbucket.conf, jenkins.conf, rocketchat.conf として作成します。内容はほぼ同一で、listenするポート番号とproxy_passぐらいしか違いがないので、ここではgitbucket.confのみを例として挙げておきます。

# gitbucket.conf
server {
    # HTTPS
    listen 8280 ssl;
    listen [::]:8280 ssl;
    server_name ドメイン;

    ssl on;
    ssl_certificate /etc/letsencrypt/live/ドメイン/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ドメイン/privkey.pem;

    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDH !aNULL !eNULL !SSLv2 !SSLv3';
    ssl_prefer_server_ciphers on;
    add_header Strict-Transport-Security "max-age=15768000; includeSubdomains";

    location / {
        proxy_pass http://gitbucket:8080;
        proxy_redirect off;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $http_host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

proxy_passの値として、jenkins.confでは http://jenkins:8080, rocketchat.confでは http://rocket.chat:3000 をそれぞれ指定します。

また、共通的な部分は reverse_proxy.conf としてくくり出しておきます。

# reverse_proxy.conf
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

ここまで設定が済んだらdocker-compose upでコンテナを起動します。

それではまずGitBucketから見て行きましょう。ブラウザで https://ドメイン:8280/にアクセスし、ログイン前の画面が表示されればOKです。いつも通りログインし、右上のプルダウンから “System administration” を開き、”System settings” のBase URLを https://ドメイン名:8280 に変更して “Apply Changes” で保存します。

次にJenkinsです。ブラウザでhttps://ドメイン:8281/にアクセスします。こちらも同様にログイン画面が開きますのでログインします。「Jenkinsの管理」から「システムの設定」を開き、Jenkins URLを https://ドメイン:8281/ に、末尾のBuild Server URLも https://ドメイン:8281/ にそれぞれ変更して保存します。

続いてジョブの一覧から作成済みのジョブを選択し、「設定」を開いてGitBucketのURL欄を https://ドメイン:8280/〜 に変更します。

最後にRocket.Chatです。https://ドメイン:8282/にアクセスしてログインします。そうするとWarningダイアログが表示されてURLを変更するか確認してきますので「はい」を選択します。

これで、HTTPSで各サービスにアクセスできるようになりました。

2017/02/24追記: 変更を保存し忘れたりすると、サイトにアクセスできなくなります。というかなりました。私の場合、./gitbucket/data/gitbucket.confの base_url の値を直接編集して事無きを得ました。危ない危ない。

Let’s EncryptでSSL証明書を取得する

ごく基本的ではありますが、CIツールの連携ができてきました。ここらでセキュリティにも気を配りましょう。平文が流れるHTTPのままではセキュリティ的に不安ですので、通常はSSLで通信路を暗号化したHTTPSを利用します。

これまでは、このHTTPS化のためにSSL証明書を有料で取得する必要がありましたが、無償のSSL証明書を発行するLet’s Encryptの登場でこの状況が一変しました。今回はこのLet’s EncryptからSSLを取得したいと思います。セットアップする環境はDebian 8 Jessieです。

まずはjessie-backportsを有効にし、Let’s encryptのクライアントアプリであるcertbotをapt-getでインストールします。-t jessie-backportsを忘れずに付けましょう。

$ sudo echo 'deb http://ftp.debian.org/debian wheezy-backports main' >> /etc/apt/sources.list
$ sudo apt-get update
$ sudo apt-get install certbot -t jessie-backports

次に、certbotコマンドが認証に利用する80番ポートと443番ポートで動いているサービスを止めます。HTTPまたはHTTPSデーモンがインストール済みであれば、systemctl stopで止めましょう。

準備ができたらcertbotコマンドを実行します。-dオプションの引数には、今回SSL証明書をインストールするサーバのFQDNを指定します。ドメインとサブドメインを複数指定できたりもするようですが、今回は話を単純にするため、ドメインは一つだけ指定します。

$ sudo certbot certonly --standalone -d ドメイン

するとUIが起動しますので、質問に回答しながら進めていきます。まずは連絡先メールアドレスを求められますので入力します。次に何かのメッセージが表示された後にライセンスが表示されますので Agree しましょう。すると認証が行なわれ、証明書が /etc/letsencrypt/live/ドメイン ディレクトリにインストールされます。lsコマンドで確認してみましょう。

$ sudo ls /etc/letsencrypt/live/ドメイン
cert.pem  chain.pem  fullchain.pem  privkey.pem

なお、生成される証明書の有効期限は90日後のようです。証明書の有効期限はcronでチェック、更新されるようなので特別な設定等は不要なようです。

certbot実行のために止めたサービスは忘れず起動しておきましょう。

JenkinsとGitBucketを連携させる

前回はJenkinsとRocket.Chatを連携し、ジョブの実行結果をチャンネルに表示できるようにしました。今回はJenkinsとGitBucketを連携し、masterブランチにpushされたらジョブを実行するようにします。

まずはGitBucketにリポジトリを作成します。画面右上の “+” ボタンから “New repository” を選択し、”Create a new repository” 画面を埋めて “create repository” ボタンで作成できます。リポジトリができたら画面左側の “Settings” から設定画面を開き、”Service Hooks” タブを開いておきましょう。

次にJenkinsの設定です。「Jenkinsの管理」から「プラグインの管理」を選択し、「利用可能」タブから “GitBucket Plugin” を選択し、前回の “RocketChat Notifier Plugin” と同様にインストールします。

プラグインをインストールしたらJenkinsのトップページに戻り、プロジェクトの一覧から前回作成した “jenkins-rocketchat” を選択します。画面左側の「設定」を開くと、”GitBucket”欄が追加されていますので、ここに先ほど作成したリポジトリのURLを入力し、その下の “Enable hyperlink to the issue” にチェックを入れます。

続いて「ビルド・トリガ」欄の “Build when a change is pushed to GitBucket” にチェックを入れたら設定を保存します。

これだけではまだ不十分です。画面右上のユーザ名からユーザページを開き、左側の「設定」を選択します。「APIトークンの表示…」ボタンをクリックするとAPIトークンが表示されますので、これをコピーします。

ここでGitBucketに戻ります。”Service Hooks”タブの “Add webhook” ボタンを選択してフィールドを埋めます。

  • Payload URL: 今回の設定では http://jenkins:8080/gitbucket-webhook/
  • Content type: application/x-www-form-urlencodedのまま
  • Security Token: ここにAPIトークンをペーストします
  • Which events would you like to trigger this webhook?: 今回はPushにチェック

入力したら、”Payload URL” 欄右側の “Test Hook” ボタンをクリックして設定を確認します。問題なければ “WebHook Test” ダイアログが表示され、 “Response” タブに “200” と表示されます。

問題なければダイアログを閉じ、”Add webhook” ボタンでWebHookを追加します。これで設定は完了です。

冒頭に作成したリポジトリをローカルにcloneし、README.mdあたりを編集してgit commit, masterリポジトリにpushしてみて下さい。Jenkinsの “jenkins-rocketchat” ジョブがスケジューリング後に実行され、履歴が表示されます。また、Rocket.Chatのjenkinsチャンネルにビルドの開始・終了が通知されていることと思います。

JenkinsとRocket.Chatを連携させる

Notofication from Jenkins
Jenkinsからの通知

前回の記事で連携のための準備はできていますので、さっそく連携させてみましょう。

Jenkinsで「新規ジョブ作成」からジョブを作成します。ここでは「フリースタイル・プロジェクトのビルド」を選択し、タスク名を “jenkins-rocketchat” としておきましょう。

Jenkins Job Setting of Rocket.Chat
Jenkinsジョブ設定 (Rocket.Chat)

連携の動作確認のためのジョブなので、実際のビルド等は実行させません。ジョブの設定画面最下部の「ビルド後の処理の追加」プルダウンから “RocketChat Notifications” を選択すると、通知できる項目が表示されます。今回はテストなのですべてにチェックを入れておきます。右側にある「高度な設定…」から投稿先のチャンネルを変更できたりしますが、今回は触らないで設定を保存します。

作成したジョブ (プロジェクト)のページが開きますので、「ビルド実行」を選択します。するとジョブが実行され、ビルド履歴が増えます。Rocket.Chatの “jenkins” チャンネルを開くと、Jenkinsのビルド開始と終了の通知メッセージが届いていることを確認できます。

JenkinsとRocket.Chatを連携させる準備をする

さて、ここまででGitBucket, Jenkins, Rocket.Chatの各サービスをインストール・設定してきました。個々のサービスを個別に使うだけでなく、連携させることでより便利なCIを実現できます。

今回はJenkinsとRocket.Chatを連携させて、Jenkinsで発生したイベントをRocket.Chatのチャンネルに自動的に通知するようにしてみます。

まずはRocket.ChatにJenkins用のアカウントを追加します。アカウントを作成したら、Jenkinsからの通知を受け取るチャンネルを作成します。このチャンネルに普段使っているアカウントと、今作成したJenkins用のアカウントを登録します。ここでは “jenkins” アカウントと “jenkins” チャンネルを作成したことにします。

次はJenkinsの設定です。「Jenkinsの管理」画面から「プラグンの管理」を選択し、「利用可能」タグから “RocketChat Notifier” にチェックを入れて「ダウンロードして再起動後にインストール」を選択します。ダウンロードが成功したら「インストール完了後、ジョブがなければJenkinsを再起動する」にチェックを入れるとJenkinsが再起動し、RocketChat Notifierプラグインがインストールされます。

インストールに成功したら再度「Jenkinsの設定」に戻り、「システムの設定」を開きます。ページ末尾に “Global RocketChat Notifier Settings” がありますので各フィールドを埋めて保存します。

Jenkins Setting of RocketChat
JenkinsのRocket.Chat設定
  • Rocket Server URL: docker-compose.ymlの設定に合わせて http://rocket.chat:3000
  • Login Username: この記事の冒頭で設定したアカウント “jenkins”
  • Login password: アカウント “jenkins” に設定したパスワード
  • Channel: この記事の冒頭で設定したチャンネル “jenkins”
  • Build Server URL: 空欄のままでOK

右下にある “Test Connection” で “Success” と表示されれば疎通OKですので、設定を保存しておきましょう。これで連携のための準備は完了です。

Rocket.Chatを初期設定する

続いてRocket.Chatも設定します。docker-compose.ymlの設定に従って、ブラウザで8082番ポートにアクセスすると、ログイン画面が表示されます。インストール直後はアカウントが作成されていませんので、「新しいアカウントを登録」へ進みます。

新規アカウントの登録フォームを埋めて登録すると、管理アカウントとして登録されます。その後、ROOT_URLの設定が間違っていれば修正するかの確認があり、問題なければユーザ名を登録確認して利用開始です。

さて、この状態では登録フォームから自由にアカウントを作成できるようになっています。これを無効にしましょう。画面左上のユーザ名が表示されているプルダウンを選択し、「管理」から「アカウント」選択し、”Registration”の”EXPAND”で展開される項目から「登録フォームへのアクセス」の値を「無効」に変更し、「変更を保存」でOKです。

Jenkinsを初期設定する

Unlock Jenkins
Unlock Jenkins

先の記事ではJenkinsをDockerイメージとして起動しましたが、当然このままでは使えません。設定に従ってブラウザで8081番ポートにアクセスすると、”Unlock Jenkins”という画面が表示されてパスワードの入力を求められます。

このパスワードは自動生成されて、初回起動時にターミナルに出力されます。docker-composeで起動したログを確認すると、冒頭の方に * で囲まれた32桁のランダム文字列が表示されていると思います。

jenkins        | Jenkins initial setup is required. An admin user has been created and a password generated.
jenkins        | Please use the following password to proceed to installation:
jenkins        | 
jenkins        | ********************************
jenkins        | 
jenkins        | This may also be found at: /var/jenkins_home/secrets/initialAdminPassword

見逃がしていた場合でも、この出力にあるように /var/jenkins_home/secrets/initialAdminPassword に保存されていますのでご安心を。以下のようにdockerコマンドを使ってファイルの中身を確認できます。

$ docker exec -it jenkins cat /var/jenkins_home/secrets/initialAdminPassword
********************************

が、イメージ上のボリューム /var/jenkins_homeは ./jenkins/home がマウントされたものですので、直接 ./jenkins/home/secrets/initialAdminPassword を確認することもできます。

$ cat ./jenkins/home/secrets/initialAdminPassword********************************

ここで確認できたパスワードをJenkinsのUnlock Jenkins画面の入力欄にコピペすれば、晴れてJenkinsを使えるようになります。

Unlockできたらカスタマイズ画面 (プラグインの選択)が表示されます。ここでは無難に “Install suggested plugins” を選択しておきましょう。自動で複数のプラグインがインストールされ、管理アカウントの作成画面になります。フィールドを埋めてアカウントを作成したら、いよいよJenkinsが利用可能になります。