本記事では、マルウェアスキャン機能を標準搭載しない Amazon FSx for Windows File Server に対し、GuardDuty Malware Protection for S3 と Lambda を組み合わせてAWSリソースだけで自動スキャン・自動隔離を実現する手順をハンズオン形式で解説します。
サードパーティ製ウイルス対策ソフト不要で、CloudFormation によるインフラ構築からテストファイルによる動作確認まで一通り行います。
AWSのサンプルアーキテクチャをもとに進めていきます。
元記事は以下の記事となります。
[aws-samples/sample-fsx-for-windows-malware-scan]
Amazon FSx for Windows File Server とは
Amazon FSx for Windows File Serverは、フルマネージド型の Windows ネイティブファイルシステムサービスです。オンプレミスの Windows ファイルサーバーと同様に SMB プロトコルや NTFS、Active Directory 統合をそのまま利用できるため、既存の Windows アプリケーションをほぼ変更なしに移行できます。
Amazon FSx for Windows File Serverでのマルウェアスキャンの課題
ただmAmazon FSx for Windows File Serverでマルウェアスキャン(ウイルス対策)機能は標準で備わっていません。
また、フルマネージドサービスであり、OSレベルへのアクセスが制限されているため、エージェント型のアンチウイルスソフトをそのままインストールできない課題があります。
そのためファイルサーバーに対して格納されるデータへスキャンを実施し、マルウェア検知を行いたい場合は別にEC2を立ち上げて、サードパーティーの製品を導入するなどを行う必要がありました。
Amazon FSx for NetApp ONTAPであればvscan機能が備わっており、サードパーティー製品にオフロードすることで実施できていましたが、Amazon FSx for Windows File Serverはあまり事例がなくマルウェアスキャンのハードルが高かったです。
Amazon FSx for Windows File Server のマルウェアスキャンをAWS内で完結させる
今回は、AWSのBlogでサンプルソリューションとして公開された sample-fsx-for-windows-malware-scan を使って、Amazon FSx for Windows File Server へのファイルアップロードを自動でマルウェアスキャンする仕組みを構築するハンズオンを紹介します。
サーバーレスアーキテクチャで実装されているため、製品EoLを気にしなくてよい、EC2と比較して可用性が高いなどのメリットがあります。
アーキテクチャ

(https://github.com/aws-samples/sample-fsx-for-windows-malware-scanより引用)
今回のアーキテクチャは図の通りです。
FSx for Windows File Serverに格納されたデータをS3にコピーを行い、GuardDuty Malware Protection for S3によるマルウェアスキャンを行い、マルウェアが検知された場合は隔離します。
GuardDuty Malware Protection for S3とは
Amazon GuardDuty Malware Protection for S3 は、S3 バケットにアップロードされたオブジェクトをリアルタイムで自動スキャンし、マルウェア・ランサムウェア・スパイウェアなどの脅威を検知するフルマネージドサービスです。エージェントのインストールや定義ファイルの管理は不要で、AWS が提供する脅威インテリジェンスと機械学習モデルを使って未知の脅威にも対応します。スキャン結果はオブジェクトにタグとして付与され、EventBridge 経由で後続処理(隔離・通知など)へ連携できます。
詳細は以下記事を参照ください。
AWS公式ブログ:FSx for Windows の自動マルウェアスキャン
すなわち、Amazon FSx for Windows File Serverでマルウェアスキャンができないため、S3とGuardDuty Malware Protection for S3を使用したマルウェア検知機能と連携させてスキャンを行おうという仕組みです。
環境構築
それでは環境構築を行っていきます。
サンプルアーキテクチャ内にCloudFormationのyamlファイルは格納されていますが、事前準備としてADのドメイン環境やVPCなどの作成が必要です。
事前準備
今回は事前のCloudFormationを実行します。
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
DomainName:
Type: String
Default: test.internal
Description: Managed AD domain name (FQDN)
AllowedPattern: '^([a-zA-Z0-9]+\.)+[a-zA-Z]{2,}$'
DomainShortName:
Type: String
Default: TEST
Description: NetBIOS domain name (up to 15 characters)
MaxLength: 15
DomainAdminPassword:
Type: String
NoEcho: true
MinLength: 8
MaxLength: 64
Description: >
AD Admin password (8-64 characters).
Must NOT contain the word "admin".
Must include at least 3 of the following 4 categories:
uppercase (A-Z), lowercase (a-z), digits (0-9), special characters (!@#$%^ etc.)
Example: MyP@ssw0rd123
KeyPairName:
Type: AWS::EC2::KeyPair::KeyName
Description: Key pair for Windows EC2 instance
WindowsAmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-windows-latest/Windows_Server-2022-Japanese-Full-Base
Description: Windows Server AMI (no change required)
WindowsInstanceType:
Type: String
Default: t3.medium
AllowedValues:
- t3.medium
- t3.large
- m5.large
Description: Windows EC2 instance type
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Active Directory Settings
Parameters:
- DomainName
- DomainShortName
- DomainAdminPassword
- Label:
default: EC2 Settings
Parameters:
- KeyPairName
- WindowsAmiId
- WindowsInstanceType
ParameterLabels:
DomainName:
default: AD Domain Name (FQDN)
DomainShortName:
default: NetBIOS Domain Name
DomainAdminPassword:
default: AD Admin Password
KeyPairName:
default: Key Pair Name
WindowsAmiId:
default: Windows AMI ID
WindowsInstanceType:
default: Instance Type
Resources:
# ----------------------------------------------------------
# VPC
# ----------------------------------------------------------
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-vpc
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-igw
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
# ----------------------------------------------------------
# Subnets
# PublicSubnet1 (AZ-a) : 10.0.0.0/24 - NAT Gateway / Windows EC2
# PrivateSubnet1 (AZ-a) : 10.0.10.0/24 - Managed AD / FSx / Lambda
# PrivateSubnet2 (AZ-b) : 10.0.11.0/24 - Managed AD (requires 2 AZs)
# ----------------------------------------------------------
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.0.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-public-1
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.10.0/24
AvailabilityZone: !Select [0, !GetAZs '']
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-private-1
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.11.0/24
AvailabilityZone: !Select [1, !GetAZs '']
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-private-2
# ----------------------------------------------------------
# Public Route Table (via Internet Gateway)
# ----------------------------------------------------------
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-public-rt
PublicRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
# ----------------------------------------------------------
# NAT Gateway
# Required for Lambda in private subnet to reach S3 / Secrets Manager
# ----------------------------------------------------------
NatGatewayEIP:
Type: AWS::EC2::EIP
DependsOn: VPCGatewayAttachment
Properties:
Domain: vpc
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-nat-eip
NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGatewayEIP.AllocationId
SubnetId: !Ref PublicSubnet1
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-nat
# ----------------------------------------------------------
# Private Route Table (via NAT Gateway)
# ----------------------------------------------------------
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-private-rt
PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable
PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable
# ----------------------------------------------------------
# AWS Managed Microsoft Active Directory
# Used for ManagedADId in fsx-malware-scan.yaml
# ----------------------------------------------------------
ManagedAD:
Type: AWS::DirectoryService::MicrosoftAD
Properties:
Name: !Ref DomainName
ShortName: !Ref DomainShortName
Password: !Ref DomainAdminPassword
Edition: Standard
VpcSettings:
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
# ----------------------------------------------------------
# Windows EC2 (FSx mount / test client)
# ----------------------------------------------------------
WindowsInstanceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${AWS::StackName}-windows-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- arn:aws:iam::aws:policy/AmazonSSMDirectoryServiceAccess
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-windows-role
WindowsInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref WindowsInstanceRole
WindowsClientSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Windows EC2 client
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3389
ToPort: 3389
CidrIp: 0.0.0.0/0
Description: RDP access (restrict source IP in production)
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-windows-sg
WindowsEC2:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref WindowsAmiId
InstanceType: !Ref WindowsInstanceType
SubnetId: !Ref PublicSubnet1
SecurityGroupIds:
- !Ref WindowsClientSG
IamInstanceProfile: !Ref WindowsInstanceProfile
KeyName: !Ref KeyPairName
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-windows-client
# Auto domain join via SSM after Managed AD is ready
DomainJoinAssociation:
Type: AWS::SSM::Association
DependsOn: ManagedAD
Properties:
Name: AWS-JoinDirectoryServiceDomain
Targets:
- Key: InstanceIds
Values:
- !Ref WindowsEC2
Parameters:
directoryId:
- !Ref ManagedAD
directoryName:
- !Ref DomainName
dnsIpAddresses:
- !Select [0, !GetAtt ManagedAD.DnsIpAddresses]
- !Select [1, !GetAtt ManagedAD.DnsIpAddresses]
# ============================================================
# Outputs - use these values as parameters for fsx-malware-scan.yaml
# ============================================================
Outputs:
OutputVpcId:
Description: "fsx-malware-scan.yaml > VpcId"
Value: !Ref VPC
Export:
Name: !Sub ${AWS::StackName}-VpcId
OutputSubnetId:
Description: "fsx-malware-scan.yaml > SubnetId (private subnet)"
Value: !Ref PrivateSubnet1
Export:
Name: !Sub ${AWS::StackName}-SubnetId
OutputManagedADId:
Description: "fsx-malware-scan.yaml > ManagedADId"
Value: !Ref ManagedAD
Export:
Name: !Sub ${AWS::StackName}-ManagedADId
OutputVpcCidr:
Description: "fsx-malware-scan.yaml > VpcCidr"
Value: 10.0.0.0/16
Export:
Name: !Sub ${AWS::StackName}-VpcCidr
OutputLayerBucketName:
Description: "fsx-malware-scan.yaml > LayerBucketName (created by prepare-layer.sh)"
Value: !Sub lambda-layer-smbprotocol-${AWS::AccountId}
Export:
Name: !Sub ${AWS::StackName}-LayerBucketName
WindowsEC2PublicIp:
Description: "Windows EC2 public IP address (RDP)"
Value: !GetAtt WindowsEC2.PublicIp
ManagedADDnsIp1:
Description: "Managed AD DNS IP address 1"
Value: !Select [0, !GetAtt ManagedAD.DnsIpAddresses]
ManagedADDnsIp2:
Description: "Managed AD DNS IP address 2"
Value: !Select [1, !GetAtt ManagedAD.DnsIpAddresses]
こちらの実行後、VPCやManagedAD、操作用のEC2を作成します。
その後はサンプルアーキテクチャに沿って環境構築を行います。
smbprotocol.zipの格納
まずはprepare-layer.shを実行します。
prepare-layer.shをよく見るとap-southeast-2とリージョンがハードコーディングされているため、シェルの実行前にデプロイしたいリージョンに書き換えてから実行してください。(この検証では東京リージョンで検証しています。)
sed -i 's/ap-southeast-2/ap-northeast-1/g' prepare-layer.sh
ただ、リージョンの置き換えをしても今回うまくいかなかったためprepare-layer.shをあきらめて手動で以下のコマンドを実行しています。
東京リージョンでうまくいかない場合は参考にしてください。
git clone https://github.com/aws-samples/sample-fsx-for-windows-malware-scan
cd sample-fsx-for-windows-malware-scan/scripts
sudo yum install python3.12 -y
python3.12 -m venv venv
source venv/bin/activate
mkdir python && cd python
pip install smbprotocol cffi==1.17.1 -t .
cd ..
zip -r smbprotocol.zip python
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
BUCKET_NAME="fsx-malware-layer-${ACCOUNT_ID}"
aws s3api create-bucket --bucket ${BUCKET_NAME} --region ap-northeast-1 --create-bucket-configuration LocationConstraint=ap-northeast-1
aws s3 cp smbprotocol.zip s3://${BUCKET_NAME}/
aws s3 ls s3://${BUCKET_NAME}/
smbprotocol.zipがアップロードされていれば問題ありません。

このリポジトリは 2025 年 11 月に作成されましたが、本記事執筆時点(2026 年 3 月)では cffi 2.0.0 が最新版となっており、Lambda の Python 3.12 ランタイムと非互換です。prepare-layer.shを直接実行しても内部で pip install smbprotocol -t . を実行しているため、バージョンを固定しない限り同様の問題が発生します。cffi==1.17.1 を明示的に指定することで回避できました。
fsx-malware-scan.yamlのデプロイ
続いて以下のfsx-malware-scan.yamlをデプロイしていきます。
fsx-malware-scan.yaml(GitHub)
事前に作成したCloudFormationスタックから必要な情報を取得してパラメータに格納していきます。
必要な情報はアウトプットで指定しているため出力タブから確認しCloudFormationを実行します。
メールアドレスは検知時の通知先のものとしてご自身のアドレスを入力してください。
デプロイ後、SNS から確認メールが届くので 「Confirm subscription」をクリックすることで環境準備は完了です。
デプロイ後設定
それではデプロイ後の準備を行っていきます。
Amazon FSx for Windows File Server
まずはAmazon FSx for Windows File ServerのDNS名を控えます。
リソースを開いて、ネットワークとセキュリティタブのDNS名を控えてください。
Secrets Manager の認証情報の設定
次にLambdaが取得するSecrets Managerの値です。
デフォルトではSecretString: '{"username":"username","password":"password"}'の設定のため、使用するユーザー、パスワードを設定します。今回は最初に使用したCloudFormationでの認証情報を入れました。

タグ付の有効化
GuardDuty Malware Protection for S3のタグ付が有効になっていないため、有効化します。
タグ付けが無効のままでは、GuardDuty によるスキャンは行われますが、スキャン結果がオブジェクトに付与されないため、後続の隔離処理やメール通知が機能しません。

EC2への接続
続いて、EC2に接続をしていきます。
上記のCloudFormationを使用していた場合、パブリックサブネットに検証用のEC2があります。
IPアドレスを控えて、RDPクライアントで接続します。
- ユーザー名: スタック作成時に指定したドメイン\Admin
- パスワード: スタック作成時に指定したパスワード
ユーザー名はCloudFormationを書き換えて任意のユーザー名にすることが可能です。
Windows Defender のリアルタイム保護の無効化
今回はEICARのテストファイルを用いてマルウェア検知時の動作確認を行います。
EICARファイルを作成時にOS標準で保護されているWindows Defenderが検知を行うため、テスト中のみ無効化します。
手順
- スタートメニュー → Windows Security を開く
- ウイルスと脅威の防止 → ウイルスと脅威の防止の設定項目の設定の管理
- リアルタイム保護 を オフ にする
確認がありますが、はいをクリックして設定を変えてください。


上記の画面のようになっていれば対応完了です。
テストが終わればこちらは再度有効化しておきます。
Amazon FSx for Windows File Serverにファイル共有をマウント
Powershellを管理者権限で開き以下のPowershellを実行します。
の箇所は控えていたDNS名を使用してください。
# FSx DNS 名を変数に設定(Step 1 で確認した値に置き換える)
$FsxDnsName = "<FSX-DNS-NAME>"
# ファイル共有をマウント
net use Z: \\$FsxDnsName\share
# 監査ルールを設定
$sharePath = "\\$FsxDnsName\share"
$auditUser = "Everyone"
$auditRights = "CreateFiles,WriteData"
$auditType = "Success"
$acl = Get-Acl $sharePath
$auditRule = New-Object System.Security.AccessControl.FileSystemAuditRule(
$auditUser, $auditRights, "ContainerInherit,ObjectInherit", "None", $auditType)
$acl.SetAuditRule($auditRule)
Set-Acl -Path $sharePath -AclObject $acl
Write-Host "Auditing configured for $sharePath"
コマンドを実行が完了するとエクスプローラーからAmazon FSx for Windows File Serverにアクセスを行い正常に接続できたことが確認できました。
長くなりましたが、以上で環境構築などの事前作業は完了です。
テストファイルのダウンロード
それではテストファイルとEICARのダウンロードを行います。
今回はAWSのサンプルアーキテクチャのgitにテスト用のファイルも格納されているためそのファイルを使用していきます。
ファイルがダウンロードできていることが確認できました。

EICARがダウンロードできない場合は、セキュリティ設定が影響している可能性があるため環境を確認ください。
正常系動作確認
では、clean.txtから動作確認を行います。
clean.txtをAmazon FSx for Windows File Serverのファイル共有にコピーを行います。
今回のclean.txtは無害なファイルのため、通常の動作を確認していきます。
確認結果

オブジェクトがS3にコピーされ、問題ないことを示すNO_THREATS_FOUNDが付与されていることが確認できました。
異常系動作確認
続いてマルウェア検知時の動作を確認します。
EICARをAmazon FSx for Windows File Serverに格納します。

30秒ほどで、EICARが隔離を示すquarantineフォルダに移動しました。


作成された quarantine フォルダのアクセス権限は制限されていないため、実運用では セキュリティ担当者のみがアクセスできるよう権限を絞ることを推奨します。

ちなみにフォルダを作成した場合でもマルウェアスキャンは実施されます。

S3バケット内から元々どのAmazon FSx for Windows File Serverのフォルダに格納されていたか確認ができます。

まとめ
以上、Amazon FSx for Windows File Serverでサードパーティー製品を用いず、AWSリソースのみでマルウェアスキャンを行いました。
監査ログをCloudWatch Logsに出力しているため、追加でどのユーザーが該当のファイルをアップロードしたかなど追跡も可能です。
AWSで完結しているためマルウェアスキャン用のEC2のメンテナンスコストや可用性、サーバーレス構成によるコスト最適化など様々な面でメリットはありそうです。
ただ、LambdaやGuardDuty Malware Protection for S3の都合上、従量課金になるため、事前に料金計算などは必要になります。GuardDuty Malware Protection for S3もサービスリリース直後に比べて料金が安くなっていたりとするため、小中規模環境では候補になりえるかと思います。





