Security AWS

KICS 2.2.0 on AWS User Guide

| Product: KICS 2.2.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the KICS 2.2.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg OWASP ZAP and Vulnerability Scanners listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

KICS (Keeping Infrastructure as Code Secure) is the open source infrastructure-as-code scanner from Checkmarx. It reads Terraform, CloudFormation, AWS CDK output, Kubernetes manifests, Helm charts, Dockerfiles, Docker Compose, Ansible, Azure Resource Manager, Google Deployment Manager, OpenAPI and more, and reports security misconfigurations before they are deployed: a security group open to the internet, a public database, an unencrypted volume, a container running as root. Each finding carries a severity, a CWE, the file and line, and the expected and actual values. Results can be written as JSON or SARIF for a pipeline gate, a code review or a dashboard.

Upstream KICS ships as a Docker image. The cloudimg image adds:

  • A native KICS 2.2.0 with no container runtime. The scanner is compiled by cloudimg from the official KICS v2.2.0 source with Go 1.27.3 and updated klauspost/compress, golang.org/x/net, containerd and OpenTelemetry modules, and installed with the full query library from the same release commit. The build provenance and an integrity check (sudo kics-verify) ship on the instance.
  • A scheduled scan that cannot phone home. A daily timer scans a directory you choose. The scan runs as its own unprivileged kics user inside a systemd sandbox with no network access at all: every address is denied, including the EC2 instance metadata service, the operating system is read-only to it, and it holds no capabilities. KICS needs no network to scan; its queries are on disk.
  • Reports only root can read. A report describes where your infrastructure is weak, so the scheduled scan writes its JSON and SARIF reports with mode 0600 into dated 0700 directories under /var/lib/kics/reports (owned by kics). Ordinary users cannot read or even list them, and the system journal gets only a one line count of findings.
  • A self-test on every new instance. On first boot the instance checks the binary and the query library against their build-time fingerprints and scans two bundled fixtures: a deliberately insecure Terraform file that must be flagged and a clean Dockerfile that must not.
  • Key-only SSH (no password or keyboard-interactive login, no root login), IMDSv2 only, and a fully patched Ubuntu 24.04 LTS with unattended security upgrades switched on.

This is a command-line product: there is no web UI, no listening service, no account and no password. SSH on port 22 is the only open port.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet. Scanning needs no outbound internet access; you only need a way to get your code onto the instance (for example git clone over a NAT gateway or a VPC endpoint).
  • A security group that allows TCP 22 (SSH) from your own networks only. No other inbound port is needed.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick KICS 2.2.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (KICS runs its queries in parallel, so more vCPUs make large scans faster), your key pair and a security group as above. Size the root volume for the code you will scan.

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <sg-allowing-22-from-your-network> --subnet-id <subnet-id> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=kics}]'

Step 2: Connect to your instance

Operating system SSH user
Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<instance-public-ip>

There is no password to look up: /etc/kics/credentials on the instance is an informational note that says so and lists the paths used below.

Step 3: Check the installation

kics version
systemctl is-enabled kics-scan.timer kics-firstboot.service
systemctl is-active kics-scan.timer
Keeping Infrastructure as Code Secure 2.2.0
enabled
enabled
active

kics-firstboot.service ran once on the first boot of your instance and started the self-test. kics-scan.timer runs the scheduled scan once a day. Confirm the self-test passed:

sudo journalctl -u kics-selftest.service -o cat --no-pager | grep SELFTEST
kics-scan-run: SELFTEST OK: insecure fixture exit 60 (total=14 critical=1 high=3 medium=4 low=2 info=4 files=1 queries=1101); clean fixture exit 0 with 0 findings

The self-test takes about two minutes of CPU, so on a brand new instance give it a minute or two before you look.

Step 4: Scan the insecure fixture

/usr/share/kics/fixtures/insecure/main.tf is a small Terraform file with an SSH security group open to 0.0.0.0/0 and a publicly accessible, unencrypted RDS instance. Never apply it. Scan it and write JSON and SARIF reports to ~/kics-out/insecure:

kics scan --no-progress --no-color --ci -p /usr/share/kics/fixtures/insecure -o ~/kics-out/insecure --report-formats json,sarif > ~/kics-out-insecure.log 2>&1; echo "exit code: $?"
grep -A 18 'Queries Summary' ~/kics-out-insecure.log
exit code: 60
7:37PM INF --- Queries Summary ---
Platform     Query Name                                        Severity    CWE    Results

Terraform    RDS DB Instance Publicly Accessible               CRITICAL    668    1
Terraform    DB Instance Storage Not Encrypted                 HIGH        311    1
Terraform    Sensitive Port Is Exposed To Entire Network       HIGH        200    1
Terraform    Unrestricted Security Group Ingress               HIGH        668    1
Terraform    IAM Database Auth Not Enabled                     MEDIUM      285    1
Terraform    RDS With Backup Disabled                          MEDIUM      754    1
Terraform    RDS Without Logging                               MEDIUM      778    1
Terraform    Security Group With Unrestricted Access To SSH    MEDIUM      732    1
Terraform    IAM Access Analyzer Not Enabled                   LOW         710    1
Terraform    Tags Not Copied to RDS Cluster Snapshot           LOW         284    1
Terraform    Resource Not Using Tags                           INFO        665    2
Terraform    Security Group Not Used                           INFO        284    1
Terraform    Security Group Rule Without Description           INFO        710    1

The JSON report has the counts and every finding with its file and line:

jq '{total_counter, queries_total, files_scanned, severity_counters}' ~/kics-out/insecure/results.json
jq -r '.queries[] | "\(.severity)  \(.query_name)  \(.files[0].file_name | split("/")[-1]):\(.files[0].line)"' ~/kics-out/insecure/results.json | head -4
{
  "total_counter": 14,
  "queries_total": 1101,
  "files_scanned": 1,
  "severity_counters": {
    "CRITICAL": 1,
    "HIGH": 3,
    "INFO": 4,
    "LOW": 2,
    "MEDIUM": 4,
    "TRACE": 0
  }
}
CRITICAL  RDS DB Instance Publicly Accessible  main.tf:19
HIGH  DB Instance Storage Not Encrypted  main.tf:20
HIGH  Sensitive Port Is Exposed To Entire Network  main.tf:6
HIGH  Unrestricted Security Group Ingress  main.tf:10

KICS loads only the queries for the platforms it finds, which is why a Terraform-only scan runs 1,101 of the 1,813 queries on the image.

KICS flagging the open security group and the public, unencrypted RDS instance in the insecure fixture

Step 5: Confirm a clean scan reports nothing

/usr/share/kics/fixtures/clean/Dockerfile pins its base image by digest, runs as a non-root user and has a health check:

kics scan --no-progress --no-color --ci -p /usr/share/kics/fixtures/clean -o ~/kics-out/clean --report-formats json > ~/kics-out-clean.log 2>&1; echo "exit code: $?"
jq '.total_counter' ~/kics-out/clean/results.json
exit code: 0
0

Step 6: Scan your own infrastructure code

Point -p at a directory or a single file. As an example, a Dockerfile and a Terraform S3 bucket with a public ACL:

mkdir -p ~/iac-demo
cat > ~/iac-demo/Dockerfile <<'EOF'
FROM ubuntu:latest
RUN apt-get update && apt-get install -y curl
COPY app /app
CMD ["/app/run"]
EOF
cat > ~/iac-demo/bucket.tf <<'EOF'
resource "aws_s3_bucket" "logs" {
  bucket = "example-logs-bucket"
}

resource "aws_s3_bucket_acl" "logs" {
  bucket = aws_s3_bucket.logs.id
  acl    = "public-read"
}
EOF
kics scan --no-progress --no-color --ci -p ~/iac-demo -o ~/kics-out/demo --report-formats json > ~/kics-out-demo.log 2>&1; echo "exit code: $?"
jq -r '.queries[] | "\(.severity)  \(.platform)  \(.query_name)  \(.files[0].file_name | split("/")[-1]):\(.files[0].line)"' ~/kics-out/demo/results.json
exit code: 60
CRITICAL  Terraform  S3 Bucket ACL Allows Read Or Write to All Users  bucket.tf:7
HIGH  Dockerfile  Missing User Instruction  Dockerfile:1
MEDIUM  Dockerfile  Apt Get Install Pin Version Not Defined  Dockerfile:2
MEDIUM  Dockerfile  Image Version Using 'latest'  Dockerfile:1
MEDIUM  Terraform  S3 Bucket Logging Disabled  bucket.tf:1
MEDIUM  Terraform  S3 Bucket Without Versioning  bucket.tf:1
LOW  Dockerfile  Healthcheck Instruction Missing  Dockerfile:1
LOW  Terraform  IAM Access Analyzer Not Enabled  bucket.tf:1
INFO  Dockerfile  APT-GET Not Avoiding Additional Packages  Dockerfile:2
INFO  Dockerfile  Apt Get Install Lists Were Not Deleted  Dockerfile:2
INFO  Terraform  Resource Not Using Tags  bucket.tf:1

Each finding in results.json also has description, expected_value, actual_value and a link to the query's documentation, which is usually enough to fix it. To scan one of your own repositories, clone it and point -p at it:

git clone https://github.com/<your-org>/<your-infra-repo>.git ~/work/infra
kics scan --no-progress --ci -p ~/work/infra -o ~/kics-out/infra --report-formats json,sarif

SARIF and the CWE taxonomy. KICS adds the CWE taxonomy to a SARIF report only when it finds its CWE tables relative to the current directory, so for SARIF you upload to GitHub code scanning or an IDE, run it from /opt/kics/bin with absolute paths (the scheduled scan does this for you):

(cd /opt/kics/bin && kics scan --no-progress --ci -p /usr/share/kics/fixtures/insecure -o ~/kics-out/sarif --report-formats sarif > /dev/null 2>&1)
jq -r '.runs[0].taxonomies[].name' ~/kics-out/sarif/results.sarif
Categories
CWE

Network use. When you run kics scan yourself, KICS makes one HTTPS request to api.github.com to see whether a newer KICS release exists; it sends nothing about your code or findings, and with no internet access it skips the check silently. The scheduled scan cannot make it at all (Step 9).

Step 7: The exit code contract for CI

KICS exits with a code for the highest severity it found, which makes it usable as a merge gate:

Exit code Meaning
0 No findings
20 Highest severity found: INFO
30 Highest severity found: LOW
40 Highest severity found: MEDIUM
50 Highest severity found: HIGH
60 Highest severity found: CRITICAL
1 The scan did not complete

--fail-on chooses which severities count for the exit code; anything else exits 0. To fail a pipeline only on CRITICAL and HIGH:

kics scan --no-progress --ci -p ~/iac-demo -o ~/kics-out/gate --report-formats json --fail-on critical,high > /dev/null 2>&1; echo "fail-on critical,high exit: $?"
kics scan --no-progress --ci -p /usr/share/kics/fixtures/clean -o ~/kics-out/gate-clean --report-formats json --fail-on critical,high > /dev/null 2>&1; echo "clean fixture exit: $?"
fail-on critical,high exit: 60
clean fixture exit: 0

The KICS exit codes for a failing and a clean scan, and the scheduled scan configuration

Step 8: The scheduled scan and its reports

The scheduled scan reads /etc/kics/scan.conf, scans SCAN_TARGET (default /srv/iac) and writes a dated report directory plus a latest link. Copy the example code into it, run one now and read its log:

sudo cp -r ~/iac-demo /srv/iac/iac-demo
sudo systemctl start kics-scan.service
sudo journalctl -u kics-scan.service -n 4 --no-pager -o cat
kics-scan-run: scanning /srv/iac with KICS (formats json,sarif)
kics-scan-run: FINDINGS (total=11 critical=1 high=1 medium=4 low=2 info=3 files=2 queries=1149) - see /var/lib/kics/reports/kics-scan-20261107T193937Z
kics-scan.service: Deactivated successfully.
Finished kics-scan.service - Scheduled KICS infrastructure-as-code scan of SCAN_TARGET (cloudimg).

systemctl start waits for the scan to finish, which takes a minute or so. The reports belong to the kics user and only root can read them:

sudo ls -l /var/lib/kics/reports/latest/
ls /var/lib/kics/reports 2>&1 || true
-rw------- 1 kics kics  2385 Nov  7 19:40 kics.log
-rw------- 1 kics kics 12515 Nov  7 19:40 results.json
-rw------- 1 kics kics 34417 Nov  7 19:40 results.sarif
ls: cannot access '/var/lib/kics/reports': Permission denied

Read a report with sudo. results.sarif is SARIF 2.1.0, which GitHub code scanning, GitLab and most IDEs import directly:

sudo jq '.severity_counters' /var/lib/kics/reports/latest/results.json
sudo jq -r '.runs[0].tool.driver.name' /var/lib/kics/reports/latest/results.sarif
{
  "CRITICAL": 1,
  "HIGH": 1,
  "INFO": 3,
  "LOW": 2,
  "MEDIUM": 4,
  "TRACE": 0
}
KICS

The scheduled scan running as the kics user, its counts-only journal line and its 0600 reports

A scan that finds something exits with the severity code from Step 7; systemd treats 0, 20, 30, 40, 50 and 60 as normal results, and the journal line FINDINGS (...) - see <directory> tells you which report to open. The scheduled scan runs as the kics user, so everything under SCAN_TARGET must be readable by that user: files 0644 and directories 0755, or group kics. To collect reports from another system, copy them off as root (for example sudo cp into a directory you control) and decide there whether they may leave the instance.

Step 9: What the scheduled scan can reach

The scheduled scan's sandbox is set in its systemd unit:

systemctl show -p User -p WorkingDirectory -p IPAddressDeny -p ProtectSystem -p ProtectHome -p ReadWritePaths -p NoNewPrivileges -p CapabilityBoundingSet kics-scan.service
IPAddressDeny=0.0.0.0/0 ::/0
WorkingDirectory=/opt/kics/bin
CapabilityBoundingSet=
User=kics
ReadWritePaths=/var/lib/kics
ProtectHome=read-only
ProtectSystem=strict
NoNewPrivileges=yes

IPAddressDeny=0.0.0.0/0 ::/0 is how systemd reports the unit's deny rules, the instance metadata service (169.254.169.254) and every other address: the scan cannot open any network connection, so a malicious file in a scanned repository cannot use it to reach the instance metadata service or anything else. The self-test and first boot units carry the same rules. The only path the scan may write to is /var/lib/kics, so keep REPORT_DIR under it.

Step 10: Configuration

grep -v '^#' /etc/kics/scan.conf | grep .
SCAN_TARGET="/srv/iac"
REPORT_DIR="/var/lib/kics/reports"
REPORT_FORMATS="json,sarif"
FAIL_ON="critical,high,medium,low,info"
EXTRA_ARGS=""
REPORT_RETENTION_DAYS=30
  • SCAN_TARGET: the directory (or file) the daily scan reads.
  • REPORT_DIR and REPORT_RETENTION_DAYS: where reports go (keep it under /var/lib/kics) and how many days of dated reports are kept.
  • REPORT_FORMATS: any of the formats kics scan --report-formats accepts, for example json,sarif,html,junit.
  • FAIL_ON: the severities that count for the exit code and the FINDINGS journal line.
  • EXTRA_ARGS: extra kics scan flags, for example --exclude-paths /srv/iac/vendor or --type terraform,dockerfile to limit the platforms.

The file is read at the start of every scan, so a change takes effect on the next scan. Edit it with sudo nano /etc/kics/scan.conf. To change the schedule, add a drop-in with sudo systemctl edit kics-scan.timer (for example OnCalendar=hourly). The default is OnCalendar=daily with up to 30 minutes of random delay.

Step 11: How the scanner on this image was built

sudo kics-verify
binary         OK  sha256 954de7fb7da36efe5b771d35043b317e4102aa27a8bc9bea801b824b6d8c1fd9
query library  OK  3668 95ed41a3824a65af0780b72f00cdad7ec38f01fb7a4c6f6eb6668233747e36bb

kics-verify recomputes the binary's hash and a manifest of every file in the query library and compares them with the values recorded at build time; first boot runs it before the self-test. The full record:

cat /usr/share/kics/build-provenance/BUILD-PROVENANCE.txt
KICS 2.2.0 on this image was compiled by cloudimg from the unmodified upstream source at
https://github.com/Checkmarx/kics.git commit 453066aa9c66014ad7aee7dd0149ba1944c78050 (tag v2.2.0), with the module bumps below to clear Go security
advisories. Licence: Apache-2.0 (/usr/share/kics/LICENSE.upstream). KICS is a Checkmarx open-source project.

toolchain     go1.27.3 linux/amd64, https://go.dev/dl/go1.27.3.linux-amd64.tar.gz sha256 65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40
modules       go get github.com/klauspost/compress@v1.20.2 golang.org/x/net@v0.62.0 github.com/containerd/containerd@v1.7.36 go.opentelemetry.io/otel/sdk@v1.45.0
              (module proxy + sum.golang.org); minimum version selection raised the other lines in go.mod.diff.
              go.mod sha256 7a856afba59fa5a66e97c68500344bb6f8c7afb2214af8c0db1c679d461023c0, go.sum sha256 4c0ce348eeda987735b303f3cb762263742f7b9f3d2e96e82e38bad7e1ac874f (pinned at build)
flags         CGO_ENABLED=0 go build -trimpath
              -ldflags "-X github.com/Checkmarx/kics/v2/internal/constants.Version=2.2.0 -X github.com/Checkmarx/kics/v2/internal/constants.SCMCommit=453066aa9c66014ad7aee7dd0149ba1944c78050"
              (not stripped; no Sentry DSN; no descriptions endpoint)
govulncheck   v1.9.0 -mode=binary on the compiled binary: GO-2026-5064 GO-2026-5338 GO-2026-5622 GO-2026-5932 GO-2026-6791
              (GO-2026-5064/5338/5622/6791 = containerd CRI checkpoint restore, containerd/v2 >= 2.1.0 only, CRI not linked;
               GO-2026-5932 = golang.org/x/crypto/openpgp deprecation, no fixed version exists)
queries       /opt/kics/bin/assets: assets/{queries,libraries,cwe_csv,similarityID_transition} at the same commit
              (git trees 42ca44ce4c8042c4d8086c3b0cf0700841ead48f 1cdb355a60c3321f91f8b58e893625af92d9b0c9 25cc837d21e2b96a8776190973e979e68c32be34 23642ca6b27f7ffccab960025d52016f42b67735),
              1813 queries, shipped without the per-query test/ fixture directories;
              manifest (count sha256) 3668 95ed41a3824a65af0780b72f00cdad7ec38f01fb7a4c6f6eb6668233747e36bb  -- re-check: sudo kics-verify
upstream      source tarball v2.2.0.tar.gz sha256 3be94ee0b393699b72f00be8603260c60d5531c61aada740cfcbbc3009d1ef05

The remaining advisories do not affect KICS: four are in the containerd CRI checkpoint-restore code of containerd/v2 2.1 and later, which this binary does not link, and no fixed version of golang.org/x/crypto/openpgp exists. The same directory holds the module list compiled into the binary (buildinfo.txt), the exact go.mod change (go.mod.diff) and the full govulncheck report.

Step 12: Access to the instance

sudo sshd -T | grep -E '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin|pubkeyauthentication) '
permitrootlogin no
pubkeyauthentication yes
passwordauthentication no
kbdinteractiveauthentication no

SSH accepts only the key pair you chose at launch (/etc/ssh/sshd_config.d/10-cloudimg-key-only.conf), and no account on the image has a password. Nothing else listens on a public address.

Step 13: Components

Component Path
KICS binary /opt/kics/bin/kics (linked from /usr/local/bin/kics)
Query library /opt/kics/bin/assets
Scheduled scan runner /usr/local/bin/kics-scan-run
Scan, self-test and first boot units /etc/systemd/system/kics-scan.service, kics-scan.timer, kics-selftest.service, kics-firstboot.service
Configuration /etc/kics/scan.conf
Reports /var/lib/kics/reports (kics, mode 0700)
Default scan target /srv/iac
Self-test fixtures /usr/share/kics/fixtures
Integrity check /usr/local/sbin/kics-verify
Build provenance and licence /usr/share/kics/build-provenance/, /usr/share/kics/LICENSE.upstream
Key-only SSH drop-in /etc/ssh/sshd_config.d/10-cloudimg-key-only.conf

Step 14: Security recommendations

  • Allow port 22 in the security group from your own networks only.
  • Treat every report as sensitive: it is a map of where your infrastructure is weak. Keep reports under /var/lib/kics and copy them off only to systems you trust.
  • Fix CRITICAL and HIGH findings before you apply the code, and gate merges on them with --fail-on critical,high.
  • Keep the operating system patched; unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else. New KICS releases, with their new queries, arrive as new versions of this listing.

Support and licensing

KICS is open source under the Apache License 2.0 (a copy is at /usr/share/kics/LICENSE.upstream); KICS is a Checkmarx open source project, and this image is not affiliated with or endorsed by Checkmarx. There is no per user or per CPU licence fee. The AMI price covers cloudimg's build, hardening, first boot setup and support.

  • Email: support@cloudimg.co.uk
  • Website: https://www.cloudimg.co.uk
  • Support: 24/7, with a 24 hour response SLA