How can files on AWS be scanned for viruses? There is no built-in solution. But we can build our own using Lambda Functions and Layers.
Our solution involves packing the open-source antivirus engine ClamAV into a Lambda Layer and running the scan command from a Lambda Function.
This article only covers how to package ClamAV into a Lambda layer for use in Lambda functions.
Get the required ClamAV files
The first step in building the Lambda Layer is to obtain the necessary files from the RPM package in the Amazon Linux repository. To achieve this, we run a Dockerfile with the amazonlinux image and install ClamAV, then extract the files.
First, let’s create a config file for ClamAV called freshclam.conf.
DatabaseMirror db.de.clamav.net database.clamav.net
CompressLocalDatabase no
ScriptedUpdates no
LogVerbose yes
Next, create a file called dockerfile.
Although AWS states that the package name clamav should point to the latest ClamAV version, it only does when you run dnf install. When calling the dnf download command still points to the outdated version 0.103.12. Thats why we download explicitly clamav1.4.
# © 2025 cloudxs GmbH. All rights reserved. / dockerfile
FROM amazonlinux:2023
WORKDIR /home/build
RUN set -e
RUN echo "Prepping ClamAV"
RUN rm -rf bin
RUN rm -rf lib
RUN dnf update -y
RUN dnf install -y cpio
dnf-plugins-core
zip
RUN dnf download --arch x86_64 clamav1.4
RUN rpm2cpio clamav1.4-1*.rpm | cpio -vimd
RUN dnf download --arch x86_64 clamav1.4-lib
RUN rpm2cpio clamav1.4-lib*.rpm | cpio -vimd
RUN dnf download --arch x86_64 clamav1.4-update
RUN rpm2cpio clamav1.4-freshclam*.rpm | cpio -vimd
RUN dnf download --arch x86_64 json-c
RUN rpm2cpio json-c*.rpm | cpio -vimd
RUN dnf download --arch x86_64 pcre2
RUN rpm2cpio pcre*.rpm | cpio -vimd
RUN dnf download --arch x86_64 libtool-ltdl
RUN rpm2cpio libtool-ltdl*.rpm | cpio -vimd
RUN dnf download --arch x86_64 libxml2
RUN rpm2cpio libxml2*.rpm | cpio -vimd
RUN dnf download --arch x86_64 bzip2-libs
RUN rpm2cpio bzip2-libs*.rpm | cpio -vimd
RUN dnf download --arch x86_64 xz-libs
RUN rpm2cpio xz-libs*.rpm | cpio -vimd
RUN dnf download --arch x86_64 gnutls
RUN rpm2cpio gnutls*.rpm | cpio -vimd
RUN dnf download --arch x86_64 nettle
RUN rpm2cpio nettle*.rpm | cpio -vimd
RUN dnf download --arch x86_64 openldap
RUN rpm2cpio openldap*.rpm | cpio -vimd
RUN dnf download --arch x86_64 pcre
RUN rpm2cpio pcre*.rpm | cpio -vimd
RUN dnf download --arch x86_64 nss
RUN rpm2cpio nss*.rpm | cpio -vimd
RUN dnf download --arch x86_64 libssh2
RUN rpm2cpio libssh2*.rpm | cpio -vimd
RUN mkdir -p bin
RUN mkdir -p lib
RUN mkdir -p var/lib/clamav
RUN chmod -R 777 var/lib/clamav
COPY ./freshclam.conf .
RUN cp usr/bin/clamscan usr/bin/freshclam bin/.
RUN cp -R usr/lib64/* lib/.
RUN cp freshclam.conf bin/freshclam.conf
# Copy some libraries separately
RUN cp /lib64/libcurl* lib/.
RUN cp /lib64/libcrypt* lib/.
RUN cp /lib64/libnss* lib/.
RUN cp /lib64/libunistring* lib/.
RUN cp /lib64/libgcrypt* lib/.
RUN cp /lib64/libssl* lib/.
RUN cp ./usr/lib64/libssh2* lib/.
RUN cp /lib64/libidn2* lib/.
RUN cp /lib64/libnghttp2* lib/.
RUN cp ./usr/lib64/libsmime3* lib/.
RUN yum install shadow-utils.x86_64 -y
RUN groupadd clamav
RUN useradd -g clamav -s /bin/false -c "Clam Antivirus" clamav
RUN useradd -g clamav -s /bin/false -c "Clam Antivirus" clamupdate
RUN LD_LIBRARY_PATH=./lib ./bin/freshclam --config-file=bin/freshclam.conf
RUN zip -r9 clamav_lambda_layer.zip bin
RUN zip -r9 clamav_lambda_layer.zip lib
RUN zip -r9 clamav_lambda_layer.zip var
RUN zip -r9 clamav_lambda_layer.zip etc
The Dockerfile above creates a new container with ClamAV and all its required libraries in a ZIP archive within the container. This means we need to extract it. For this, we have a bash script called build.sh that builds the container, runs it, copy the layer.zip and extract into a directory called layer.
#!/bin/bash
# © 2025 cloudxs GmbH. All rights reserved. / build.sh
set -e
rm -rf ./layer
mkdir layer
docker build -t clamav -f Dockerfile --progress=plain .
docker run --name clamav clamav
docker cp clamav:/home/build/clamav_lambda_layer.zip .
docker rm clamav
mv clamav_lambda_layer.zip ./layer
pushd layer
unzip -n clamav_lambda_layer.zip
rm clamav_lambda_layer.zip
popd
Now, all the files we need to run ClamAV in a Lambda function are in the directory called layer.
Create the Lambda Layer
Of course, deploying Lambda Functions and Lambda Layers depend a lot on the IAC tool you use. We use SST, which is Pulumi under the hood.
To create a Lambda Layer with SST or Pulumi, all you need to do is package the layer directory from the previous step. Our deployment looks similar to this.
const avLayerOutputDir = 'workspaces/antivirus/lambda/clamav_bin/layer';
const layerAvBinaries = new aws.lambda.LayerVersion(
`clamav-bin-${$app.stage}`,
{
layerName: `clamav-bin-${$app.stage}`,
code: $asset(avLayerOutputDir),
compatibleRuntimes: ['nodejs20.x', 'nodejs22.x'],
},
);
CI/CD pipeline
We build and deploy the Lambda Layer in an Azure DevOps pipeline. To improve pipeline speed, we implemented a caching step. In order to make proper use of the caching, we need to know the most current ClamAV version. The following fragment is the build stage of our pipeline.
variables:
- name: CACHE_HIT_LAMBDA_LAYER
value: 'false'
pool:
vmImage: ubuntu-latest
stages:
- stage: Build
jobs:
- job: build_lambda_layer
displayName: Build Lambda Layer
steps:
- bash: bash getClamavVersion.sh
displayName: Get ClamAV version
workingDirectory: $(System.DefaultWorkingDirectory)/workspaces/antivirus/lambda/clamav_bin
- task: Cache@2
displayName: Cache lambda layer
inputs:
key: 'amazonlinux | "$(Agent.OS)" | "Clamav $(clamav_version)"'
path: $(System.DefaultWorkingDirectory)/workspaces/antivirus/lambda/clamav_bin/layer
cacheHitVar: CACHE_HIT_LAMBDA_LAYER
- bash: bash build.sh
displayName: Build lambda layer
workingDirectory: $(System.DefaultWorkingDirectory)/workspaces/antivirus/lambda/clamav_bin
condition: ne(variables.CACHE_HIT_LAMBDA_LAYER, 'true')
- script: ls -lisah
displayName: List files in the working directory
workingDirectory: $(System.DefaultWorkingDirectory)/workspaces/antivirus/lambda/clamav_bin/layer
- publish: $(System.DefaultWorkingDirectory)/workspaces/antivirus/lambda/clamav_bin/layer
artifact: layer
The first step in the build job calls the script getClamavVersion.sh. This script runs the amazonlinux container, gets the latest clamav version and writes it into an pipeline variable.
#!/bin/bash
# © 2025 cloudxs GmbH. All rights reserved. / getClamavVersion.sh
AZLINUX_IMAGE="amazonlinux:2023"
CLAMAV_PKG="clamav1.4"
docker run --rm $AZLINUX_IMAGE bash -c "CAVERSION=$(dnf info $CLAMAV_PKG | grep Version | awk -F: '{print $2}');
echo "##vso[task.setvariable variable=clamav_version]$CAVERSION""
We hope this article helps anyone who is facing the challenges of packaging the ClamAV antivirus in a Lambda Layer.
Sincerely, Andy from cloudxs GmbH
