タグ: Nginx

  • このサイトのSSL設定ミスっていました

    昨年10月頃に設定を見直したときに、中間証明書を入れ忘れていました。
    Safari は何もなく接続可能でしたが、Chrome は証明書の警告が出てしまう状態でした。
    おそらく、Chrome ユーザが離脱されたことと思います。残念です。

    今後はSSL 設定変更後に複数ブラウザで確認した方が良さそうですね。
    もしくは、SSL 解析を行ってくれるサイトを使うのも良いと思います。

    https://www.ssllabs.com/ssltest/

    中間証明書がない時は評価「B」でしたが、今は「A+」になりました。👍

  • Spring Boot + Nginx でセキュリティ対策を大雑把に実施する

    業務でセキュリティ診断を受けました。
    数回の診断を経て、最終的には「指摘なし」との高評価をいただくことができました。
    この記事では、高評価に至るまでに実施した事を残したいと思います。

    なお、お約束ではありますが、セキュリティは要件次第で対策内容が変わるものです。
    以下の内容は、大雑把にセキュリティ対応を行うためのもので、万全を保証するものではありません。


    今回の構成

    • Spring Boot (3.4.1)
    • Spring Security
    • Thymeleaf
    • バニラJavaScript (react やvue などを使わない)
    • MyBatis
    • Nginx

    Spring Boot

    application-prd.yml に以下の設定を入れます。

    YAML
    server:
      servlet:
        session:
          cookie:
            secure: true   # (1)
            name: __Host-SESSION   # (2)

    これらの設定はHTTPS 環境でないと動かないので、application-prd.yml に記載してapplication.yml (-prdなし)に記載しないと良いです。
    こうすることでローカル環境でもSpring Boot に直接アクセスできます。

    (1) Cookie中のセッションID をSecure 属性で保護します

    付けなかった場合は、HTTP で通信を行った時にもcookie が送付されるため、セッションID が漏えいしてしまい、セッションを乗っ取られる可能性があります。
    とても重要な設定です。

    (2) Cookie中のセッションIDをホストオンリーとします

    セッションIDの名前がデフォルトのJSESSIONID から変更されます。
    __Host- が先頭につくことでサブドメインからのCookie 注入を防いでくれます。
    こちらを設定した場合、Cookie にドメインを指定できなくなります。(する必要がない)


    Spring Security

    以下の設定を入れます。

    Java
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.headers((headers) -> headers
            .contentSecurityPolicy(config -> config
                .policyDirectives("default-src 'self'")
            ) // (3)
        ).csrf(csrf -> csrf
            .csrfTokenRepository(new HttpSessionCsrfTokenRepository())
        ); // (4)
        return http.build();
    }

    (3) Content security policy (CSP)の適用

    CSP を適用することで、ブラウザに意図しない外部サイトのスクリプト実行を制限させます。
    クロスサイトスクリプティング対策として有用です。
    Nginx で設定することも可能ですが、開発時にSpring Boot へ直接アクセスすることがあるので、Spring Security で設定した方が良いと思います。加えて、nonce という例外を書く機能が使えるようになるメリットがありますが、私の場合は最後まで使うことはありませんでした。

    設定した場合に、開発上の注意点が2つあります。

    ひとつは、JS、CSS、画像が外部から読み込めなくなることです。
    CDN など外部から読み込みが必要な場合は、例外の設定が必要になります。
    私の場合、Google recaptcha が必要だったため、以下のような例外設定を追記しました。

    default-src 'self’;
    script-src 'self' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/;
    frame-src https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/;
    connect-src 'self' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/;

    もうひとつは、onclick= やstyle= などのインライン属性が使えなくなります。
    こちらはフロント側の開発に大きく影響します。

    onclick についてはイベント形式に書き直します。

    ❌ CSP で制限される書き方

    HTML
    <span onclick="alert('クリックされた!')">クリック可能な文字列</span>

    ✅ CSP 対応済みの書き方

    HTML
    <span data-action="alert">クリック可能な文字列</span>
    JavaScript
    document.querySelectorAll('[data-action="alert"]').forEach(btn => {
      btn.addEventListener('click', () => {
        alert('クリックされた!')
      })
    })

    style= についてはCSS に必要なクラスを作成して対応します。

    ❌ CSP で制限される書き方

    HTML
    <span style="display:none">非表示</span>

    ✅ CSP 対応済みの書き方

    HTML
    <span class="app-display-none">クリック可能な文字列</span>
    CSS
    .app-display-none {
    	display: none;
    }

    当初私は style が使えないことを面倒だと感じましたが、外観をCSS で全て管理できるので綺麗な書き方になると考え直しました。

    (4) CSRF 対策

    攻撃者がXSS などで任意のJavaScript を実行できる場合に、被害者のログイン済みサイトから送金操作などのデータ操作を行うことを防ぎます。
    この設定を入れると、form タグにトークンが自動的に追加されます。

    HTML
    <form action="/action" method="post"> 
      <input type="hidden" name="_csrf" value="...長いトークン...">
      ...
    </form>

    2行目をSpring Security が自動的に挿入します。

    同時に、Spring Securityがトークンの検証を行うため、意図しないページからのPOST アクセスを防ぐことができます。
    注意点として、GETリクエストには適用されないことが挙げられます。
    HTTP の原則に従って、GET でデータ操作を行わないというルールを守る必要があります。

    バニラJavaScript(フレームワークを使わないJavaScript のこと)でサーバへリクエストを投げる(fetch)場合は、このトークンを付与することで投げることができます。
    これは、攻撃者も同じことを行うことが可能なのですが、前述のCSP 対応と後述のThymeleaf によるXSS 対策で防ぐことになります。

    今回の構成から外れますが、SPA の場合は、cookie を使った対策をフレームワークで行います。
    私が経験のあるAngular について、リンクを置いておきます。


    Nginx

    以下の設定を入れます。

    # (5)
    server_tokens off;
    # (6)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
    # (7)
    add_header Referrer-Policy "same-origin, origin-when-cross-origin" always;

    (5) Nginx のバージョンを表示しない

    デフォルトではエラー画面やレスポンスヘッダーにNginx のバージョンが記載されます。
    万一、使っているNginx のバージョンに脆弱性が見つかった場合、バージョン番号が攻撃者に有用な情報となってしまいます。

    (6) Strict-Transport-Security ヘッダを設定

    HTTPS を使っており、HTTP を使わないことをブラウザに覚えさせる設定です。
    一度このヘッダーを受信したブラウザは、タイムアウトするまで(2年間)常にHTTPS でアクセスします。

    (7) Referrer にパスを入れない

    パスに大切な情報が含まれると情報漏洩につながる恐れがあるので、念の為外部サイトにパスを送付しない設定を入れます。

    origin-when-cross-origin を指定すると、同一オリジンにはReferrer を送るが、クロスオリジンにパスを送付しなくなります。この機能は一部のブラウザで未対応らしです。
    same-origin を指定すると、外部サイトにReferrer を送らなくなります。origin-when-cross-origin 未対応のブラウザはこちらが有効になります。


    Thymeleaf

    クロスサイトスクリプティング(XSS)対策として、文字列を代入するときに、th:text の使用を徹底します。

    ✅ 安全な書き方

    HTML
    <span th:text="${name}">ユーザ名</span>

    th:utext を使うとHTML を代入することができますが、問題を起こす可能性が上がるので極力使いません。
    要件次第ではありますが、私が携わったプロジェクトでは一切使わずに済みました。

    ❌ 問題の可能性が上がる書き方

    HTML
    <span th:utext="${html}">動的HTML</span>

    その他のフレームワークを使っている場合でも、似たような機能があるはずです。


    MyBatis

    SQL インジェクション対策として、基本的には$ を使わず# を使うように徹底します。
    しかし、order by だけは # で直接指定することができません。
    代わりに<choose>を使います。

    ✅ 安全な書き方

    SQL
    SELECT
      id,
      name,
      email,
      created_at
    FROM users
    <choose>
      <when test="orderBy == 'name'">
        ORDER BY name
      </when>
      <when test="orderBy == 'createdAt'">
        ORDER BY created_at
      </when>
      <otherwise>
        ORDER BY id
      </otherwise>
    </choose>

    くれぐれもユーザが指定する値を直接$ で渡すことは避けてください。

    ❌ 問題の可能性が上がる書き方

    SQL
    SELECT ... FROM users ORDER BY ${orderBy}

    その他

    IPA の「安全なウェブサイトの作り方」を見ると、上で触れていない問題があります。

    触れてはいませんが、構成上問題になりにくいものと、私の場合要件にないため問題にならないものがありましたので、分類しておきます。

    上で触れたもの
    1 SQLインジェクション
    4 セッション管理の不備
    5 クロスサイト・スクリプティング
    6 CSRF(クロスサイト・リクエスト・フォージェリ)

    構成上問題になりにくいもの
    9 クリックジャッキング (Spring Security 使用のため)
    10 バッファオーバーフロー (Java 使用のため)

    要件にないので問題にならなかったもの
    2 OSコマンド・インジェクション
    3 パス名パラメータの未チェック/ディレクトリ・トラバーサル
    7 HTTPヘッダ・インジェクション
    8 メールヘッダ・インジェクション

    こちらは今回の記事の対象外とさせてください。
    11 アクセス制御や認可制御の欠落


    感想

    脆弱性診断で高評価を得られたことで、今回の記事を書く動機が湧きました。

    勝因は、自前でセキュリティ対策のコードを書かずに、Spring、Nginx、Mybatis といった巨人に委ねた点だと考えています。

    実装にあたって特に面倒だと感じたのは CSP、Thymeleaf、Mybatis のところでした。ですが、一度正しく設定して、雛形をチーム内で共有するところまでたどり着いた後は、スムーズに開発できたと思っています。チーム内で不満の声は上がらなかったです。

    くどいですが、これらの対策を行えば万全というものではなく、何もしないよりこれらをやっておいた方が無難というものになります。
    抜け道を作ることや設定を緩めることは可能ですが、その際は要件と設計とセキュリティをよく検討の上実施ください。

    【広告】

  • nginx on docker の運用

    さくらVPSを1台レンタルし、その上で複数のサービスを運用している。http リクエストをnginx で受けて、背後のサービスへリバースプロキシで連携する構成である。

    nginx とサービスA-C がコンテナとなっている。

    この構成でしばらく運用するには主に以下の作業が必要となる。

    • nginx 静的コンテンツ入れ替え
    • nginx バージョンアップ
    • nginx ログ参照
    • サービス増加
    • SSL 証明書更新

    これらをなるべく簡単に行うために、次の点を工夫した。

    1. docker volume によるファイル管理
    2. Makefile を使ったコマンドの簡略化

    1. docker volume によるファイル管理

    volume を使用することで、コンテナから見えるファイルをホストOS 上で操作できるようになる。ホストOSからコンテナへのファイル授受は多少手間なので活用したい。
    対象としたいファイルは以下。

    • nginx 設定ファイル
    • nginx インクルードファイル
    • nginx ログ
    • ドキュメントルート
    • SSL 証明書

    docker-compose で起動するためのyml ファイルはこのようになる。

    compose.yml
    
    services:
      nginx:
        # nginx を最新化するためlatest を指定
        image: nginx:latest
    
        # OS起動時は常に立ち上げておく
        restart: always
    
    
        # タイムゾーンを東京にしておく
        environment:
          - TZ=Asia/Tokyo
    
        # コンテナと共有するフォルダ
        volumes:
          # nginx 設定ファイル
          - ./nginx/nginx.conf:/etc/nginx/nginx.conf
          # nginx インクルードファイル
          - ./nginx/conf.d:/etc/nginx/conf.d
          # nginx ログ
          - /opt/nginx/log:/var/log/nginx
          # SSL証明書など秘密ファイル
          - /opt/secret:/opt/secret
          # ドキュメントルート
          - /opt/nginx/html:/opt/nginx/html
        # ホストモードにすることで
        network_mode: host

    2. Makefile を使ったコマンドの簡略化

    よく使うコマンドは簡単に叩けるようにしておきたい。次のMakefile を作っておく。

    # make の仕様で第一引数と同じ名前のフォルダが存在すると動かなくなる
    # .PHONY: を登録しておくことで単純なコマンド実行とみなされる
    .PHONY: nginx
    
    # 再起動
    restart:
    	@make down
    	@make up
    # 起動
    up:
    	docker-compose up -d
    # 停止
    down:
    	docker-compose down
    # nginx のシェル取得
    nginx-bash:
    	docker-compose exec nginx bash
    # nginx 設定ファイル文法チェック
    test:
    	docker-compose exec nginx nginx -t
    # nginx 設定再読み込み
    reload:
    	docker-compose exec nginx nginx -s reload
    # プロセス一覧
    ps:
    	docker-compose ps
    # ログ(コンテナの標準出力)
    log:
    	docker-compose logs --follow
    # コンテナイメージ削除(down 後に実施)
    prune:
    	docker system prune -af
    # ヘルプ(引数一覧)
    help:
    	echo "usage: up,down,nginx-bash,test,reload,ps,log,prune,help"
    

    使い方は引数に渡すだけだ。参考までにいくつかの実行結果を記載しておく。

    プロセス一覧

    $ make ps 
    docker-compose ps
    NAME          IMAGE                            COMMAND                  SERVICE   CREATED          STATUS          PORTS
    prd-nginx-1   docker.io/library/nginx:latest   "nginx -g daemon off;"   nginx     20 minutes ago   Up 20 minutes   
    
    ↑ 20分前にnginx が起動している

    nginx 設定ファイル文法チェック

    $ make test
    docker-compose exec nginx nginx -t
    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
    nginx: configuration file /etc/nginx/nginx.conf test is successful

    nginx 設定再読み込み

    $ make reload
    docker-compose exec nginx nginx -s reload

    これらを組み合わせることで運用に必要な作業を簡易に行うことができる。

    nginx のバージョンアップは、make down make prune make up にて行える。

    サービス増加やSSL 証明書更新後は、make test を行い、success が確認できたらmake reload を行う。make test を行う前に再起動を行うと、もし設定に誤りがあったら起動できなくなる。

    ICOOON-MONN 様のアイコンを使わせていただきました。ありがとうございます。

    https://icooon-mono.com

    【広告】