最終更新:

コンテナセキュリティベストプラクティス: DockerとKubernetesの安全な運用


コンテナ技術の普及により、セキュリティ対策も進化しています。この記事では、Dockerイメージのセキュア化からKubernetes本番環境での防御戦略まで、実践的なセキュリティ対策を解説します。

コンテナセキュリティの基本原則

セキュリティの4つの層

  1. イメージセキュリティ: ベースイメージ、依存関係、ビルドプロセス
  2. ランタイムセキュリティ: 実行時の制限、分離、監視
  3. ネットワークセキュリティ: 通信の暗号化、アクセス制御
  4. オーケストレーションセキュリティ: RBAC、ポリシー、シークレット管理

Dockerイメージのセキュア化

最小限のベースイメージを選択

# ❌ 悪い例: フルOSイメージ(大きい、脆弱性多い)
FROM ubuntu:latest

RUN apt-get update && apt-get install -y nodejs npm

COPY . /app
WORKDIR /app
RUN npm install
CMD ["node", "server.js"]

# ✅ 良い例: Alpine Linux(小さい、最小限)
FROM node:20-alpine

# セキュリティアップデートを適用
RUN apk update && apk upgrade

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

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

マルチステージビルドで攻撃面を削減

# ビルドステージ
FROM node:20-alpine AS builder

WORKDIR /build

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# 本番ステージ(最小限)
FROM node:20-alpine AS production

# セキュリティアップデート
RUN apk update && apk upgrade && \
    apk add --no-cache dumb-init

# 非rootユーザーを作成
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# ビルド成果物のみコピー
COPY --from=builder --chown=nodejs:nodejs /build/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /build/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /build/package.json ./

USER nodejs

EXPOSE 3000

# dumb-initでゾンビプロセスを防ぐ
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

脆弱性スキャンの統合

# .github/workflows/docker-security.yml
name: Docker Security Scan

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  scan:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'

      - name: Upload Trivy results to GitHub Security
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

      - name: Run Snyk container scan
        uses: snyk/actions/docker@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          image: 'myapp:${{ github.sha }}'
          args: --severity-threshold=high

      - name: Fail on vulnerabilities
        run: |
          if [ -s trivy-results.sarif ]; then
            echo "❌ Vulnerabilities found!"
            exit 1
          fi

イメージの署名と検証

# Docker Content Trust(DCT)を有効化
export DOCKER_CONTENT_TRUST=1

# イメージに署名してプッシュ
docker trust sign myregistry.io/myapp:v1.0.0

# 署名を検証
docker trust inspect --pretty myregistry.io/myapp:v1.0.0

# Notaryを使った検証(Kubernetes)
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: signed-app
spec:
  containers:
  - name: app
    image: myregistry.io/myapp:v1.0.0
    imagePullPolicy: Always
  imagePullSecrets:
  - name: registry-credentials
EOF

Hadolintによる静的解析

# Hadolintのインストール
brew install hadolint

# Dockerfileの静的解析
hadolint Dockerfile

# CI/CDに統合
# .github/workflows/dockerfile-lint.yml
name: Dockerfile Lint

on: [push, pull_request]

jobs:
  hadolint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: Dockerfile
          failure-threshold: warning

ランタイムセキュリティ

Dockerランタイム制限

# docker-compose.yml(セキュア設定)
version: '3.8'

services:
  app:
    image: myapp:latest

    # 読み取り専用のルートファイルシステム
    read_only: true

    # 一時ファイル用のtmpfs
    tmpfs:
      - /tmp
      - /var/cache

    # capabilities を最小限に
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE  # ポート80/443バインドのみ

    # seccompプロファイル
    security_opt:
      - no-new-privileges:true
      - seccomp:./seccomp-profile.json

    # リソース制限
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

    # ネットワーク分離
    networks:
      - backend

    # 環境変数はシークレットから
    environment:
      - NODE_ENV=production
    secrets:
      - db_password
      - api_key

secrets:
  db_password:
    external: true
  api_key:
    external: true

networks:
  backend:
    driver: bridge
    internal: true  # 外部アクセス不可

Seccompプロファイル

// seccomp-profile.json
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_AARCH64"
  ],
  "syscalls": [
    {
      "names": [
        "accept4",
        "bind",
        "connect",
        "epoll_create1",
        "epoll_ctl",
        "epoll_wait",
        "listen",
        "read",
        "write",
        "close",
        "open",
        "openat",
        "stat",
        "fstat",
        "lstat",
        "socket",
        "mmap",
        "munmap",
        "brk",
        "exit_group"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

AppArmorプロファイル

# /etc/apparmor.d/docker-myapp
#include <tunables/global>

profile docker-myapp flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>

  # ネットワークアクセス
  network inet tcp,
  network inet udp,

  # ファイルシステムアクセス
  /app/** r,
  /tmp/** rw,
  /var/cache/** rw,

  # 実行可能ファイル
  /usr/bin/node ix,

  # 禁止事項
  deny /proc/sys/** w,
  deny /sys/** w,
  deny /** x,
}

Kubernetesセキュリティ

Pod Security Standards

# 名前空間にPod Security Standardsを適用
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

セキュアなPod設定

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
  namespace: production
spec:
  # サービスアカウント(自動マウントしない)
  serviceAccountName: app-service-account
  automountServiceAccountToken: false

  # securityContext(Pod レベル)
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault

  containers:
  - name: app
    image: myregistry.io/myapp:v1.0.0
    imagePullPolicy: Always

    # securityContext(コンテナレベル)
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      runAsNonRoot: true
      runAsUser: 1000
      capabilities:
        drop:
          - ALL
        add:
          - NET_BIND_SERVICE

    # リソース制限
    resources:
      requests:
        memory: "256Mi"
        cpu: "250m"
      limits:
        memory: "512Mi"
        cpu: "500m"

    # 環境変数(シークレットから)
    env:
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: password

    # ヘルスチェック
    livenessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 30
      periodSeconds: 10

    readinessProbe:
      httpGet:
        path: /ready
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5

    # 一時ファイル用のボリューム
    volumeMounts:
    - name: tmp
      mountPath: /tmp
    - name: cache
      mountPath: /var/cache

  volumes:
  - name: tmp
    emptyDir: {}
  - name: cache
    emptyDir: {}

  imagePullSecrets:
  - name: registry-credentials

Network Policy

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: app-network-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: myapp

  policyTypes:
  - Ingress
  - Egress

  ingress:
  # Ingressコントローラーからのみ受信
  - from:
    - namespaceSelector:
        matchLabels:
          name: ingress-nginx
    ports:
    - protocol: TCP
      port: 8080

  egress:
  # データベースへの接続のみ許可
  - to:
    - podSelector:
        matchLabels:
          app: postgres
    ports:
    - protocol: TCP
      port: 5432

  # 外部APIへのHTTPS接続を許可
  - to:
    - namespaceSelector: {}
    ports:
    - protocol: TCP
      port: 443

  # DNS解決を許可
  - to:
    - namespaceSelector:
        matchLabels:
          name: kube-system
    ports:
    - protocol: UDP
      port: 53

RBAC設定

# サービスアカウント
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-service-account
  namespace: production

---
# ロール(最小権限)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-role
  namespace: production
rules:
# ConfigMapの読み取りのみ
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["get", "list"]
  resourceNames: ["app-config"]

# Secretの読み取りのみ
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get"]
  resourceNames: ["db-credentials", "api-keys"]

---
# ロールバインディング
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-role-binding
  namespace: production
subjects:
- kind: ServiceAccount
  name: app-service-account
  namespace: production
roleRef:
  kind: Role
  name: app-role
  apiGroup: rbac.authorization.k8s.io

シークレット管理

# External Secrets Operator を使用
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
  namespace: production
spec:
  refreshInterval: 1h

  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore

  target:
    name: app-secrets
    creationPolicy: Owner

  data:
  - secretKey: db-password
    remoteRef:
      key: production/db/password

  - secretKey: api-key
    remoteRef:
      key: production/api/key

---
# SecretStoreの定義(AWS Secrets Manager)
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-manager
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-northeast-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa

Admission Controller(OPA Gatekeeper)

# ポリシー: 必ずリソース制限を設定
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredresources
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredResources
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredresources

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.limits.memory
          msg := sprintf("Container %v must have memory limits", [container.name])
        }

        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.limits.cpu
          msg := sprintf("Container %v must have CPU limits", [container.name])
        }

---
# 制約を適用
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredResources
metadata:
  name: must-have-resources
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces:
      - production

ランタイムセキュリティモニタリング

Falcoによる異常検知

# falco-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: falco-config
  namespace: falco
data:
  falco.yaml: |
    rules_file:
      - /etc/falco/falco_rules.yaml
      - /etc/falco/custom_rules.yaml

    json_output: true
    log_stderr: true
    log_syslog: false

    priority: warning

    outputs:
      rate: 1
      max_burst: 1000

  custom_rules.yaml: |
    - rule: Unauthorized Process in Container
      desc: Detect unexpected process execution
      condition: >
        spawned_process and
        container and
        not proc.name in (node, npm, sh, bash)
      output: >
        Unauthorized process started in container
        (user=%user.name command=%proc.cmdline container=%container.name)
      priority: WARNING
      tags: [container, process]

    - rule: Sensitive File Access
      desc: Detect access to sensitive files
      condition: >
        open_read and
        container and
        fd.name in (/etc/shadow, /etc/passwd, /root/.ssh/*)
      output: >
        Sensitive file accessed
        (user=%user.name file=%fd.name container=%container.name)
      priority: CRITICAL
      tags: [filesystem, security]

---
# FalcoのDaemonSet
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: falco
  namespace: falco
spec:
  selector:
    matchLabels:
      app: falco
  template:
    metadata:
      labels:
        app: falco
    spec:
      serviceAccountName: falco
      hostNetwork: true
      hostPID: true
      containers:
      - name: falco
        image: falcosecurity/falco:latest
        securityContext:
          privileged: true
        volumeMounts:
        - name: docker-socket
          mountPath: /host/var/run/docker.sock
        - name: dev
          mountPath: /host/dev
        - name: proc
          mountPath: /host/proc
          readOnly: true
        - name: boot
          mountPath: /host/boot
          readOnly: true
        - name: lib-modules
          mountPath: /host/lib/modules
          readOnly: true
        - name: usr
          mountPath: /host/usr
          readOnly: true
        - name: etc
          mountPath: /host/etc
          readOnly: true
        - name: config
          mountPath: /etc/falco
      volumes:
      - name: docker-socket
        hostPath:
          path: /var/run/docker.sock
      - name: dev
        hostPath:
          path: /dev
      - name: proc
        hostPath:
          path: /proc
      - name: boot
        hostPath:
          path: /boot
      - name: lib-modules
        hostPath:
          path: /lib/modules
      - name: usr
        hostPath:
          path: /usr
      - name: etc
        hostPath:
          path: /etc
      - name: config
        configMap:
          name: falco-config

イメージスキャンの自動化

# Trivy Operator for Kubernetes
apiVersion: v1
kind: Namespace
metadata:
  name: trivy-system

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: trivy-operator
  namespace: trivy-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app: trivy-operator
  template:
    metadata:
      labels:
        app: trivy-operator
    spec:
      serviceAccountName: trivy-operator
      containers:
      - name: operator
        image: aquasecurity/trivy-operator:latest
        env:
        - name: OPERATOR_NAMESPACE
          value: trivy-system
        - name: OPERATOR_TARGET_NAMESPACES
          value: production
        - name: OPERATOR_SCANNER_TRIVY_SEVERITY
          value: CRITICAL,HIGH

---
# VulnerabilityReportのカスタムリソース
apiVersion: aquasecurity.github.io/v1alpha1
kind: VulnerabilityReport
metadata:
  name: pod-myapp-abc123
  namespace: production
spec:
  artifact:
    repository: myregistry.io/myapp
    tag: v1.0.0
  registry:
    server: myregistry.io
  scanner:
    name: Trivy
    vendor: Aqua Security
    version: 0.40.0
  summary:
    criticalCount: 0
    highCount: 2
    mediumCount: 5
    lowCount: 10

まとめ

コンテナセキュリティの実践的なベストプラクティスを紹介しました。

セキュリティチェックリスト

イメージセキュリティ

  • 最小限のベースイメージを使用
  • マルチステージビルドで攻撃面を削減
  • 脆弱性スキャンをCI/CDに統合
  • イメージに署名と検証

ランタイムセキュリティ

  • 読み取り専用のルートファイルシステム
  • 最小限のcapabilities
  • seccompプロファイルを適用
  • 非rootユーザーで実行

Kubernetesセキュリティ

  • Pod Security Standards を適用
  • Network Policyでネットワーク分離
  • RBACで最小権限の原則
  • シークレットを外部管理(Vault, AWS Secrets Manager)
  • Admission Controllerでポリシー強制

モニタリング

  • Falcoでランタイム監視
  • Trivy Operatorで継続的スキャン
  • ログとメトリクスの収集

セキュリティは継続的なプロセスです。定期的な見直しと改善を行いましょう。

参考リンク