Amazon ECSコンテナでマウントされたルートファイルシステムを読み取り専用にする設定方法について

このブログシリーズ 「クラウドセキュリティ 実践集」 では、一般的なセキュリティ課題を取り上げ、「なぜ危険なのか?」 というリスクの解説から、「どうやって直すのか?」 という具体的な修復手順(コンソール、AWS CLI、Terraformなど)まで、分かりやすく解説します。
この記事では、AWS ECSのSecurity Hubコントロール「ECS-5: ECS コンテナが読み取り専用ルートファイルシステムを使用していること」について解説します。

ポリシーの説明
[ECS.5] ECS コンテナは、ルートファイルシステムへの読み取り専用アクセスに制限する必要があります。
Amazon ECS の Security Hub コントロール – AWS Security Hub
このコントロールは、Amazon ECS コンテナがルートファイルシステムへの読み取り専用アクセス権を持っているかどうかをチェックします。
readonlyRootFilesystem
パラメータが に設定されている場合false
、またはタスク定義内のコンテナ定義に パラメータが存在しない場合、コントロールは失敗します。このコントロールは、Amazon ECS タスク定義の最新のアクティブなリビジョンのみを評価します。
コンテナのルートファイルシステムを読み取り専用にすることで、コンテナ内で実行されるプロセスがファイルシステムを改ざんするリスクを軽減できます。これにより、コンテナのセキュリティが向上し、攻撃者がコンテナ内で永続的な変更を加えることが困難になります。
修復方法
AWSコンソールでの修正手順
- ECS > タスク定義へ移動し、新しいタスクの作成をクリック
- 読み取り専用ルートファイルシステムのチェックボックスをチェックオン

- 作成をクリックしタスクを作成する
Terraformでの修復手順
ECSタスク定義のReadOnlyRootFilesystem設定を有効にするためのTerraformコードと、重要な修正ポイントを説明します。
# ECS Cluster
resource "aws_ecs_cluster" "main" {
name = "ecs-cluster-${var.environment}"
setting {
name = "containerInsights"
value = "enabled"
}
tags = var.tags
}
# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "ecs" {
name = "/ecs/${var.environment}"
retention_in_days = var.log_retention_days
tags = var.tags
}
# ★重要: ECS Task Definition
resource "aws_ecs_task_definition" "app" {
family = "app-${var.environment}"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = var.task_cpu
memory = var.task_memory
execution_role_arn = aws_iam_role.ecs_task_execution.arn
task_role_arn = aws_iam_role.ecs_task_role.arn
# ★重要: コンテナ定義でReadOnlyRootFilesystemを有効化
container_definitions = jsonencode([
{
name = "app"
image = "${var.container_image}:${var.container_version}"
# ★重要: ReadOnlyRootFilesystemを有効化
readonlyRootFilesystem = true
# 書き込みが必要な場合はボリュームをマウント
mountPoints = [
{
sourceVolume = "tmp"
containerPath = "/tmp"
readOnly = false
},
{
sourceVolume = "var-run"
containerPath = "/var/run"
readOnly = false
}
]
environment = [
for k, v in var.environment_variables : {
name = k
value = v
}
]
# セキュアなログ設定
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.ecs.name
awslogs-region = data.aws_region.current.name
awslogs-stream-prefix = "ecs"
}
}
# セキュリティ設定
privileged = false
interactive = false
pseudoTerminal = false
# ヘルスチェック
healthCheck = {
command = var.healthcheck_command
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
# リソース制限
ulimits = [
{
name = "nofile"
softLimit = 65536
hardLimit = 65536
}
]
}
])
# ★重要: 必要な一時ボリュームの定義
volume {
name = "tmp"
docker_volume_configuration {
scope = "task"
autoprovision = true
driver = "local"
}
}
volume {
name = "var-run"
docker_volume_configuration {
scope = "task"
autoprovision = true
driver = "local"
}
}
tags = var.tags
}
# ECS Service
resource "aws_ecs_service" "app" {
name = "app-${var.environment}"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = var.service_desired_count
launch_type = "FARGATE"
network_configuration {
subnets = var.private_subnet_ids
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = false
}
# Circuit breaker
deployment_circuit_breaker {
enable = true
rollback = true
}
tags = var.tags
}
# Security Group for ECS Tasks
resource "aws_security_group" "ecs_tasks" {
name = "ecs-tasks-${var.environment}"
description = "Security group for ECS tasks"
vpc_id = var.vpc_id
# 最小限の通信のみ許可
ingress {
protocol = "tcp"
from_port = var.container_port
to_port = var.container_port
security_groups = var.allowed_security_group_ids
}
egress {
protocol = "-1"
from_port = 0
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(var.tags, {
Name = "ecs-tasks-${var.environment}"
})
}
# Task Execution Role
resource "aws_iam_role" "ecs_task_execution" {
name = "ecs-task-execution-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
tags = var.tags
}
# Task Role
resource "aws_iam_role" "ecs_task_role" {
name = "ecs-task-role-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
tags = var.tags
}
# Variables
variable "environment" {
description = "Environment name"
type = string
}
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "private_subnet_ids" {
description = "List of private subnet IDs"
type = list(string)
}
variable "container_image" {
description = "Container image name"
type = string
}
variable "container_version" {
description = "Container image version"
type = string
}
variable "container_port" {
description = "Container port"
type = number
}
variable "task_cpu" {
description = "Task CPU units"
type = number
default = 256
}
variable "task_memory" {
description = "Task memory (MiB)"
type = number
default = 512
}
variable "service_desired_count" {
description = "Desired number of tasks"
type = number
default = 2
}
variable "log_retention_days" {
description = "CloudWatch logs retention days"
type = number
default = 30
}
variable "environment_variables" {
description = "Environment variables for container"
type = map(string)
default = {}
}
variable "healthcheck_command" {
description = "Container healthcheck command"
type = list(string)
default = ["CMD-SHELL", "curl -f <http://localhost/> || exit 1"]
}
variable "allowed_security_group_ids" {
description = "List of security group IDs allowed to access the container"
type = list(string)
}
variable "tags" {
description = "Tags for resources"
type = map(string)
default = {}
}
# Data sources
data "aws_region" "current" {}
主要な修正ポイントは以下の通りです:
- ReadOnlyRootFilesystem の有効化(最重要):
container_definitions = jsonencode([
{
# この設定が最も重要
readonlyRootFilesystem = true
}
])
必要な書き込み可能ボリュームの追加:
# 一時ファイル用のボリューム定義
volume {
name = "tmp"
docker_volume_configuration {
scope = "task"
}
}
# コンテナ定義でのマウント
mountPoints = [
{
sourceVolume = "tmp"
containerPath = "/tmp"
readOnly = false
}
]
セキュリティ強化の追加設定:
# 特権モードの無効化
privileged = false
# インタラクティブモードの無効化
interactive = false
使用方法:
- 変数の設定:
# terraform.tfvars
environment = "production"
container_image = "your-image"
container_version = "1.0.0"
container_port = 8080
vpc_id = "vpc-xxxxx"
private_subnet_ids = ["subnet-xxxxx", "subnet-yyyyy"]
- アプリケーションの準備:
- アプリケーションがReadOnlyRootFilesystemで動作することを確認
- 必要な書き込み先を特定し、ボリュームをマウント
重要な注意点:
- アプリケーションの互換性:
- アプリケーションが読み取り専用ルートファイルシステムで動作するよう設計されていることを確認
- 必要な書き込み先を特定し、適切なボリュームをマウント
- セキュリティベストプラクティス:
# 最小権限の原則に基づいたIAMロール
aws_iam_role "ecs_task_role" {
# 必要最小限の権限のみを付与
}
# セキュリティグループの制限
aws_security_group "ecs_tasks" {
# 必要な通信のみを許可
}
監視とロギング:
# CloudWatch Logsの設定
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.ecs.name
}
}
これらの修正を適用することで:
- ルートファイルシステムの保護
- 必要な書き込み操作の保持
- 全体的なセキュリティの強化
が実現されます。
注意: この設定を適用する前に、アプリケーションがReadOnlyRootFilesystemで正常に動作することを必ずテスト環境で確認してください。
最後に
今回は、ECSコンテナが読み取り専用ルートファイルシステムを使用していることの確認と修正方法についてご紹介しました。 コンテナのルートファイルシステムを読み取り専用にすることで、セキュリティを向上させることができます。定期的なチェックと適切な設定を行い、セキュリティを強化しましょう。
この問題の検出は弊社が提供するSecurifyのCSPM機能で簡単に検出及び管理する事が可能です。
運用が非常に楽に出来る製品になっていますので、ぜひ興味がある方はお問い合わせお待ちしております。
最後までお読みいただきありがとうございました。この記事が皆さんの役に立てば幸いです。