Dockerfileセキュリティ強化ガイド:安全なイメージ設計・秘密情報・最小権限

Dockerは「動くものを早く作れる」反面、Dockerfileを雑に書くと 脆弱性の混入・権限過多・秘密情報漏えい が起きやすくなります。特に本番運用では、Dockerfileは単なるビルド手順ではなく セキュリティ境界の一部 です。

この記事では、Dockerfileの“書き方”でできるセキュリティ強化を中心に、すぐ実務に使える形でまとめます。最後に 危険なDockerfile→改善→より堅牢化 を手を動かして体感できるハンズオンも用意しました。


この記事で扱うこと(SEOを意識したキーワード)

  • Dockerfile セキュリティ / セキュアなDockerfile / ベストプラクティス
  • 非root(rootless)実行 / 最小権限 / ファイル権限
  • ベースイメージ最小化(slim / distroless)/ マルチステージ
  • 秘密情報(APIキー・トークン)をイメージに入れない
  • 依存関係固定(lock)/ 供給網(サプライチェーン)対策の入口
  • 脆弱性スキャン・SBOM・署名(運用の次の一歩)

なぜDockerfileが危険になりやすいのか

Dockerfileが抱えがちなリスクは、ざっくり次の3つに集約されます。

  1. 権限が強すぎる
    デフォルトrootで実行してしまう/不要なLinux capabilityを持ったまま動く、など。
  2. 不要なものを入れすぎる
    ビルド用ツール・シェル・パッケージが本番イメージに残り、攻撃面が増える。
  3. 秘密情報が混入する
    ENVにAPIキーを書いたり、ビルド時に設定ファイルをコピーしてレイヤーに残ったり。

Dockerは「コンテナの外に出られないから安全」と思われがちですが、現実は コンテナ突破(脆弱性、設定不備、権限過多) は普通に起こり得ます。だからこそ、Dockerfile側で “突破されても被害を最小化する” 設計が重要です。


Dockerfileセキュリティ強化チェックリスト(まずはこれ)

ここから先の内容を、実務でそのままチェックに使えるようにまとめます。

A. 最小化(Attack Surfaceを減らす)

  • ベースは必要最小限(slim / distroless を検討)
  • マルチステージでビルド用依存をruntimeに残さない
  • --no-install-recommends(Debian系)などで余計な依存を入れない
  • パッケージマネージャのキャッシュ削除(apt lists / pip cache / npm cache)

B. 最小権限(Least Privilege)

  • USER を指定して非rootで動かす
  • 実行ファイル・設定の所有者/パーミッションを適正化(chown / chmod
  • 可能なら読み取り専用で動かせる構造にする(ログはstdoutへ)

C. 再現性・供給網(Supply Chain)

  • 依存関係はlockで固定(npm/pip/aptのバージョン方針)
  • ベースイメージのタグ固定(方針により、少なくともメジャー固定など)
  • .dockerignore で意図しないファイル混入を防ぐ

D. 秘密情報(Secrets)

  • DockerfileにAPIキーを直書きしない(ENV / ARG 乱用NG)
  • ビルド時の秘密はBuildKit secrets等で渡す(レイヤーに残さない)
  • .env や鍵ファイルを COPY しない(.dockerignoreで遮断)

ハンズオン:危険なDockerfileを“安全寄り”に改善する(Node.js例)

ここでは「Dockerfileでできる対策」を体感するため、Node.jsの最小Webアプリで進めます。
目的は“完璧な要塞”ではなく、現場で効く改善を積み上げる感覚を掴むことです。

0) サンプルアプリを用意

作業ディレクトリに以下を作成します。

package.json

{
  "name": "dockerfile-secure-handson",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": { "start": "node server.js" },
  "dependencies": { "express": "^4.19.2" }
}

server.js

const express = require("express");
const app = express();

app.get("/", (_req, res) => res.send("Secure Dockerfile!"));
app.get("/health", (_req, res) => res.json({ ok: true }));

app.listen(3000, () => console.log("listening on 3000"));

1) まず“危険寄り”Dockerfile(アンチパターン)

Dockerfile.bad

FROM node:20

WORKDIR /app
COPY . .

RUN npm install
ENV API_KEY="hard-coded-secret"   # ❌ 絶対にやらない
EXPOSE 3000
CMD ["node", "server.js"]

ビルド:

docker build -t app:bad -f Dockerfile.bad .

このDockerfileの問題点:

  • ENV API_KEY=... により秘密情報が イメージに残る
  • COPY . . が早すぎて、.env や鍵などを混入しやすい
  • root実行(デフォルト)になりやすい
  • npm install は再現性が落ちがち(lock未活用)

2) .dockerignore で秘密・不要物の混入を止める(最優先)

まずは入口を塞ぎます。

.dockerignore

node_modules
npm-debug.log
.env
*.key
*.pem
.git
.gitignore
Dockerfile*

ポイント:
.env や鍵ファイルが「うっかり混入」しないように 明示的に遮断 します。Dockerfileだけ頑張っても、コンテキストに入っていたら事故ります。


3) 依存関係を分離してキャッシュ&再現性を上げる(安全にも効く)

Dockerfile.good(まずは改善版)

FROM node:20-slim

WORKDIR /app

# 依存だけ先に(キャッシュが効く+差分が明確)
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev

# アプリ本体は後で
COPY server.js ./

EXPOSE 3000
CMD ["node", "server.js"]

ビルド&起動:

docker build -t app:good -f Dockerfile.good .
docker run --rm -p 3000:3000 app:good

ここではまだ“セキュア化”の核心(非root等)が未実装ですが、まず 事故(混入)と再現性 を改善しています。


4) 非rootで動かす(Dockerfileセキュリティの核)

rootで動くコンテナは、万一侵入されたときの被害が大きくなりがちです。
Node公式イメージには node ユーザーが用意されているので、それを使います。

Dockerfile.secure(非root+権限整理)

FROM node:20-slim

WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm ci --omit=dev

# 所有者をnodeにしてから実行(権限事故を減らす)
COPY --chown=node:node server.js ./

EXPOSE 3000
USER node
CMD ["node", "server.js"]

ビルド&起動:

docker build -t app:secure -f Dockerfile.secure .
docker run --rm -p 3000:3000 app:secure

ここが重要ポイント

  • USER node で非root化
  • COPY --chown=node:node で権限を揃える(後からchownするより安全&レイヤー効率も良い)

5) マルチステージで“本番に不要なもの”を落とす(攻撃面を削る)

アプリが大きくなると、ビルド用ツールが残りがちです。
ビルド用と実行用を分けることで、実行イメージをスリムにできます。

Dockerfile.secure.multi

# ---- build ----
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY --chown=node:node server.js ./

# ---- runtime ----
FROM node:20-slim
WORKDIR /app

# 実行に必要なものだけ
COPY --from=build /app /app

EXPOSE 3000
USER node
CMD ["node", "server.js"]

ビルド:

docker build -t app:secure2 -f Dockerfile.secure.multi .
docker images | head

秘密情報(Secrets)を“イメージに残さない”実務ルール

NG例:Dockerfileに埋める(レイヤーに残る可能性)

  • ENV API_KEY=...
  • ARG TOKEN=... → 使い方次第でビルドログや履歴に残る
  • COPY .env . など

OK例:実行時に渡す(まずはこれで事故が減る)

docker run --rm -e API_KEY=xxxx -p 3000:3000 app:secure2

発展:BuildKit secrets(ビルド時に必要な秘密を“レイヤーに残さない”)

たとえばプライベートレジストリのトークン等、ビルド時にだけ必要な秘密があります。
BuildKitのsecret機能を使うと、イメージレイヤーに残さずに利用できます。

例(概念サンプル):

# syntax=docker/dockerfile:1.7
FROM node:20-slim
WORKDIR /app

COPY package.json package-lock.json* ./

# ビルド時だけ必要なトークンをsecretで渡す(レイヤーに残さない)
RUN --mount=type=secret,id=npm_token \
  sh -c 'echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > .npmrc && npm ci --omit=dev && rm -f .npmrc'

COPY --chown=node:node server.js ./
USER node
CMD ["node", "server.js"]

ビルド例(手元に npm_token.txt がある想定):

docker build --secret id=npm_token,src=npm_token.txt -t app:secure-secret .

これにより「秘密情報をDockerfileに書かない」「ビルドログやレイヤーに残さない」を両立できます。


Dockerfileだけじゃ足りない:起動時に“さらに堅くする”実務オプション(おまけ)

Dockerfileが良くても、起動時の設定で被害が増減します。最低限だけ紹介します(本番でかなり効きます)。

docker run --rm -p 3000:3000 \
  --read-only \
  --tmpfs /tmp \
  --cap-drop ALL \
  --security-opt no-new-privileges:true \
  --pids-limit 200 \
  --memory 256m \
  app:secure2
  • --read-only:ルートFSを書き込み不可(改ざん耐性が上がる)
  • --cap-drop ALL:不要capabilityを落とす(最小権限)
  • no-new-privileges:権限昇格の抑制
  • リソース制限:DoS耐性の基本

(この辺はCompose/Kubernetes側で管理することも多いので、環境に合わせて適用します)


よくある落とし穴(セキュリティ観点)

  • 「とりあえずalpine」:小さいが、ネイティブ依存でハマることがある(まずslimで安定→必要に応じて検討)
  • curl | sh をDockerfileで実行:供給網リスクが跳ね上がる(取得元固定・検証・最小化が必要)
  • apt-get updateapt-get install を別RUN:古いインデックスや余計なレイヤーを残しやすい(まとめる)
  • ログをファイルに書く設計:read-only化の邪魔になる(stdout/stderrに出す設計が運用でも安全)

まとめ:Dockerfileセキュリティ強化の最短ルート

Dockerfileで“効きが大きい順”にやるなら、次の順が現実的です。

  1. .dockerignore で混入を止める(秘密・鍵・git等)
  2. 依存分離+lockで再現性を上げる(事故と差分を減らす)
  3. USER 指定で非root化(最小権限の核)
  4. マルチステージで不要物を落とす(攻撃面を削る)
  5. secretsは「実行時」or BuildKit secretsで(レイヤーに残さない)


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です