Ansible Container Security: Image Scanning, Runtime Protection, and Supply Chain Security
By Luca Berton · Published 2024-01-01 · Category: installation
Automate container security with Ansible. Implement image scanning, runtime protection, Kubernetes security policies, and software supply chain security.
Introduction
Containers accelerate deployment but expand the attack surface. Vulnerable base images, misconfigured pods, and untrusted registries create security risks that manual review can't keep pace with. Ansible automates container security across the full lifecycle — from image scanning in CI/CD through runtime protection to supply chain verification.
See also: Ansible Secrets Management: Best Practices for Enterprise Credential Security
Image Scanning
Trivy Scanner Integration
---
- name: Scan container images for vulnerabilities
hosts: localhost
connection: local
vars:
images_to_scan:
- registry.example.com/webapp:latest
- registry.example.com/api:v2.5.0
- registry.example.com/worker:v1.8.3
severity_threshold: HIGH # CRITICAL, HIGH, MEDIUM, LOW
tasks:
- name: Install Trivy
ansible.builtin.shell: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
args:
creates: /usr/local/bin/trivy
- name: Scan images
ansible.builtin.command: >
trivy image --exit-code 1
--severity {{ severity_threshold }},CRITICAL
--format json
--output /tmp/scan-{{ item | regex_replace('[:/]', '_') }}.json
{{ item }}
loop: "{{ images_to_scan }}"
register: scan_results
failed_when: false
- name: Check for vulnerabilities
ansible.builtin.set_fact:
vulnerable_images: "{{ scan_results.results | selectattr('rc', 'ne', 0) | map(attribute='item') | list }}"
- name: Block deployment of vulnerable images
ansible.builtin.fail:
msg: |
SECURITY GATE FAILED: Vulnerable images detected:
{{ vulnerable_images | join('\n') }}
Review scan reports in /tmp/scan-*.json
when: vulnerable_images | length > 0
- name: Generate scan report
ansible.builtin.template:
src: scan-report.j2
dest: "/tmp/security-scan-{{ ansible_date_time.date }}.html"
CI/CD Gate
# In GitHub Actions or GitLab CI
- name: Security scan gate
hosts: localhost
tasks:
- name: Scan newly built image
ansible.builtin.command: >
trivy image --exit-code 1
--severity CRITICAL
--ignore-unfixed
{{ new_image_tag }}
register: scan
- name: Fail pipeline if critical vulns found
ansible.builtin.fail:
msg: "Critical vulnerabilities found — deployment blocked"
when: scan.rc != 0
Kubernetes Security Policies
Pod Security Standards
- name: Enforce Pod Security Standards
hosts: localhost
connection: local
tasks:
- name: Label namespaces with security levels
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ item.name }}"
labels:
pod-security.kubernetes.io/enforce: "{{ item.level }}"
pod-security.kubernetes.io/warn: "{{ item.level }}"
pod-security.kubernetes.io/audit: "{{ item.level }}"
loop:
- { name: production, level: restricted }
- { name: staging, level: baseline }
- { name: development, level: baseline }
Network Policies
- name: Default deny all ingress
kubernetes.core.k8s:
state: present
definition:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- name: Allow only specific traffic
kubernetes.core.k8s:
state: present
definition:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-webapp-to-api
namespace: production
spec:
podSelector:
matchLabels:
app: api
ingress:
- from:
- podSelector:
matchLabels:
app: webapp
ports:
- port: 8080
protocol: TCP
Security Context Enforcement
- name: Deploy with security context
kubernetes.core.k8s:
state: present
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-webapp
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: webapp
image: registry.example.com/webapp:v2.5.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
See also: Ansible Zero Trust Security: Implement Zero Trust Architecture for Enterprise Infrastructure
Registry Security
Private Registry with Authentication
- name: Configure container registry security
hosts: all
become: true
tasks:
- name: Create registry auth config
ansible.builtin.template:
src: registries.conf.j2
dest: /etc/containers/registries.conf
- name: Block untrusted registries
ansible.builtin.copy:
content: |
[[registry]]
prefix = "docker.io"
blocked = true
[[registry]]
prefix = "registry.example.com"
insecure = false
location = "registry.example.com"
[[registry]]
prefix = "quay.io"
insecure = false
dest: /etc/containers/registries.conf
Image Signing with Cosign
- name: Verify image signature
ansible.builtin.command: >
cosign verify
--key cosign.pub
registry.example.com/webapp:v2.5.0
register: verify_result
failed_when: verify_result.rc != 0
- name: Sign image after build
ansible.builtin.command: >
cosign sign
--key cosign.key
registry.example.com/webapp:{{ new_tag }}
environment:
COSIGN_PASSWORD: "{{ vault_cosign_password }}"
no_log: true
Runtime Security
Falco Deployment
- name: Deploy Falco runtime security
hosts: kubernetes_nodes
become: true
tasks:
- name: Deploy Falco via Helm
kubernetes.core.helm:
name: falco
chart_ref: falcosecurity/falco
release_namespace: falco-system
create_namespace: true
values:
falcosidekick:
enabled: true
config:
slack:
webhookurl: "{{ slack_security_webhook }}"
customRules:
custom-rules.yaml: |
- rule: Unexpected outbound connection
desc: Detect outbound connections from production pods
condition: >
outbound and container and
not (fd.ip in (allowed_outbound_ips))
output: >
Unexpected outbound connection
(command=%proc.cmdline connection=%fd.name
container=%container.name image=%container.image.repository)
priority: WARNING
See also: HashiCorp Vault Integration with Ansible Automation Platform: Credential Management at Scale
SBOM Generation
- name: Generate SBOM for image
ansible.builtin.command: >
trivy image --format spdx-json
--output /tmp/sbom-{{ image_name | regex_replace('[:/]', '_') }}.json
{{ image_name }}
- name: Upload SBOM to dependency tracker
ansible.builtin.uri:
url: "{{ dependency_track_url }}/api/v1/bom"
method: PUT
headers:
X-Api-Key: "{{ dependency_track_api_key }}"
body_format: form-multipart
body:
project: "{{ project_uuid }}"
bom: "{{ lookup('file', '/tmp/sbom-' + image_name | regex_replace('[:/]', '_') + '.json') }}"
Security Audit Playbook
- name: Container security audit
hosts: all
become: true
tasks:
- name: Check Docker daemon configuration
ansible.builtin.command: docker info --format '{{ "{{" }}json .SecurityOptions{{ "}}" }}'
register: docker_security
changed_when: false
- name: Verify Docker uses user namespaces
ansible.builtin.assert:
that: "'userns' in docker_security.stdout"
fail_msg: "Docker user namespace remapping not enabled"
- name: Find containers running as root
ansible.builtin.shell: |
docker ps -q | xargs -I {} docker inspect --format '{{ "{{" }}.Config.User{{ "}}" }} {{ "{{" }}.Name{{ "}}" }}' {} | grep -E '^\s' || true
register: root_containers
changed_when: false
- name: Alert on root containers
ansible.builtin.debug:
msg: "WARNING: Containers running as root: {{ root_containers.stdout_lines }}"
when: root_containers.stdout_lines | length > 0
- name: Check for privileged containers
ansible.builtin.shell: |
docker ps -q | xargs -I {} docker inspect --format '{{ "{{" }}.HostConfig.Privileged{{ "}}" }} {{ "{{" }}.Name{{ "}}" }}' {} | grep true || true
register: priv_containers
changed_when: false
- name: Fail on privileged containers
ansible.builtin.fail:
msg: "CRITICAL: Privileged containers detected: {{ priv_containers.stdout_lines }}"
when: priv_containers.stdout_lines | length > 0
Best Practices
Scan in CI, block in CD — Never deploy unscanned images Use distroless/minimal base images — Fewer packages = fewer vulnerabilities No root containers —runAsNonRoot: true and allowPrivilegeEscalation: false
Read-only filesystem — Use readOnlyRootFilesystem: true with tmpfs for write needs
Network policies everywhere — Default deny, explicit allow
Sign and verify images — Cosign for supply chain integrity
Generate SBOMs — Track dependencies for vulnerability notifications
Runtime monitoring — Falco or similar for detecting anomalous behavior
Private registries only — Block public registries in production
FAQ
How often should I scan images?
Scan on every build (CI gate) plus nightly scans of all running images — new CVEs are discovered daily against existing images.
What about vulnerability exceptions?
Use Trivy's .trivyignore file for accepted risks. Document why each exception is acceptable and set review dates.
OPA/Gatekeeper vs Pod Security Standards?
Pod Security Standards (built-in since K8s 1.25) cover common cases. OPA/Gatekeeper provides custom policies for organization-specific rules. Use both.
Conclusion
Container security automation with Ansible covers the full lifecycle — from image scanning in CI/CD through registry security, Kubernetes hardening, and runtime monitoring. By codifying security policies as Ansible playbooks, you ensure consistent enforcement across development, staging, and production environments.
Related Articles
• Ansible Kubernetes Guide • Ansible Zero Trust Security • Ansible Docker Complete GuideCategory: installation