Buildah is a command-line tool for building OCI-compatible container images without requiring a daemon or Dockerfile. Buildah supports rootless builds, overlay filesystems, and integration with podman for a complete daemonless container workflow. Buildah security requires hardening the build environment, securing build contexts, enforcing image signing, and applying supply chain security practices. This guide covers securing Buildah installations, rootless builds, Dockerfile hardening, image signing, and CI/CD integration.
Configure user namespaces:
# Check subuid/subgid configuration
cat /etc/subuid | grep $USER
cat /etc/subgid | grep $USER
# Should show ranges like:
# username:100000:65536
# username:100000:65536
# Add if missing (requires root)
sudo usermod --add-subuids 100000-165535 $USER
sudo usermod --add-subgids 100000-165535 $USER
# Verify configuration
newuidmap
newgidmap
Configure Buildah for rootless:
# Create Buildah configuration directory
mkdir -p ~/.config/containers
# Create storage configuration
cat > ~/.config/containers/storage.conf << EOF
[storage]
driver = "overlay"
runroot = "/run/user/$(id -u)/containers"
graphroot = "/home/$USER/.local/share/containers/storage"
[storage.options]
pull_options = {enable_partial_images = "false", use_hard_links = "false", ostree_repos=""}
[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"
EOF
Verify rootless mode:
# Check Buildah info
buildah info | grep -E "rootless|uid|gid"
# Should show:
# "rootless": true
# "uid": 1000
# "gid": 1000
Separate build and production environments:
# Build on isolated CI/CD hosts
# Production should only run pre-built, verified images
# Use network segmentation
# Build network: 10.0.1.0/24
# Production network: 10.0.2.0/24
# Configure firewall rules
sudo iptables -A FORWARD -s 10.0.1.0/24 -d 10.0.2.0/24 -j DROP
Restrict build host access:
# Only authorized CI/CD users should access build hosts
sudo usermod -aG build-users ci-user
# Restrict SSH access
# /etc/ssh/sshd_config
AllowGroups build-users ci-admins
# Restart SSH
sudo systemctl restart sshd
Configure build cache location:
# Set build cache directory
export BUILD_CACHE_DIR=/home/$USER/.local/share/containers/build-cache
# Set proper permissions
mkdir -p $BUILD_CACHE_DIR
chmod 700 $BUILD_CACHE_DIR
Clean build cache regularly:
# Remove unused build containers
buildah rm --all
# Remove unused images
buildah rmi --all
# Prune build cache
buildah prune
# Automate cleanup
sudo crontab -e
0 2 * * * /usr/bin/buildah prune --force >> /var/log/buildah-prune.log 2>&1
Build without privileged flag:
# BAD - Avoid privileged builds
sudo buildah bud --privileged -t myapp .
# GOOD - Rootless, non-privileged
buildah bud -t myapp .
# If privileged operations are needed, use multi-stage builds
# See Dockerfile hardening section below
Check for privileged requirements:
# Analyze Dockerfile for privileged operations
grep -E "privileged|SYS_ADMIN|NET_ADMIN" Containerfile
# Use --isolation option for stricter isolation
buildah bud --isolation chroot -t myapp .
Scan Dockerfiles for security issues:
# Install hadolint
sudo apt install hadolint # Debian/Ubuntu
sudo dnf install hadolint # RHEL/Fedora
# Lint Dockerfile
hadolint Containerfile
# Fail on errors
hadolint --failure-threshold error Containerfile
# Generate report
hadolint -f json Containerfile > lint-results.json
Common Dockerfile security issues:
# BAD - Running as root
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nginx
CMD ["nginx", "-g", "daemon off;"]
# GOOD - Non-root user
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nginx && \
useradd -r -s /bin/false nginx && \
chown -R nginx:nginx /var/cache/nginx /var/run /var/log/nginx
USER nginx
CMD ["nginx", "-g", "daemon off;"]
# BAD - Using latest tag
FROM node:latest
# GOOD - Pin version
FROM node:20.11.0-alpine3.19
# BEST - Pin digest
FROM node@sha256:abc123...
# BAD - Hardcoded secrets
ENV DB_PASSWORD=secret123
# GOOD - Use build args (still not ideal)
ARG DB_PASSWORD
# BEST - Use secrets at runtime, not build time
# BAD - Copying entire context
COPY . /app
# GOOD - Copy only needed files
COPY package*.json ./
COPY src/ ./src/
Avoid fetching from remote URLs in builds:
# BAD - Fetching from remote URL
RUN curl -fsSL https://example.com/script.sh | bash
# GOOD - Download, verify, then execute
RUN curl -fsSL -o script.sh https://example.com/script.sh && \
echo "abc123 script.sh" | sha256sum -c - && \
bash script.sh && \
rm script.sh
# BEST - Copy from build context
COPY scripts/install.sh /tmp/
RUN /tmp/install.sh && rm /tmp/install.sh
Configure network isolation:
# Build with network isolation
buildah bud --isolation chroot -t myapp .
# Or disable network entirely
buildah bud --isolation chroot --network none -t myapp .
Never use latest in Dockerfiles:
# BAD - Don't use latest
FROM ubuntu:latest
FROM node:latest
FROM python:latest
# GOOD - Pin to version
FROM ubuntu:22.04
FROM node:20.11.0-alpine3.19
FROM python:3.12.1-slim
# BEST - Pin to digest
FROM ubuntu@sha256:abc123def456...
FROM node@sha256:def456abc123...
FROM python@sha256:ghi789jkl012...
Get image digest:
# Using skopeo
skopeo inspect docker://docker.io/library/node:20 | jq -r '.Digest'
# Using buildah
buildah inspect --format '{{.Digest}}' docker.io/library/node:20
# Using crane
crane digest node:20
Scan for secrets before building:
# Install gitleaks
wget https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks-linux-amd64
chmod +x gitleaks-linux-amd64
sudo mv gitleaks-linux-amd64 /usr/local/bin/gitleaks
# Scan build context
gitleaks detect --source . --verbose
# Generate report
gitleaks detect --source . --report-format json --report-path gitleaks-report.json
# Install trufflehog
pip install truffleHog
# Scan with trufflehog
trufflehog filesystem .
Use .dockerignore to exclude sensitive files:
# .dockerignore
.git
.gitignore
.env
*.env
secrets/
credentials/
*.pem
*.key
*.crt
.ssh/
.gnupg/
Install cosign:
# Download cosign
wget https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
# Or install via package manager
sudo apt install cosign # Debian/Ubuntu (if available)
Generate signing key:
# Generate key pair
cosign generate-key-pair
# This creates:
# - cosign.key (private key - protect this!)
# - cosign.pub (public key - share this)
# Set proper permissions
chmod 600 cosign.key
chmod 644 cosign.pub
Sign image:
# Sign image with key
cosign sign --key cosign.key registry.example.com/myapp:1.0
# Sign with password-protected key
cosign sign --key cosign.key --password <password> registry.example.com/myapp:1.0
# Sign with OIDC (keyless)
cosign sign registry.example.com/myapp:1.0
Verify signature:
# Verify with public key
cosign verify --key cosign.pub registry.example.com/myapp:1.0
# Verify with OIDC (keyless)
cosign verify registry.example.com/myapp:1.0 \
--certificate-identity email@example.com \
--certificate-oidc-issuer https://accounts.google.com
# Verify with specific tag
cosign verify --key cosign.pub \
--annotation "version=1.0" \
registry.example.com/myapp:1.0
Create signature policy:
// /etc/containers/policy.json
{
"default": [
{
"type": "reject"
}
],
"transports": {
"docker": {
"registry.example.com": [
{
"type": "sigstoreSigned",
"keyPath": "/etc/containers/cosign.pub",
"signedIdentity": {
"type": "matchRepository"
}
}
],
"docker.io": [
{
"type": "insecureAcceptAnything"
}
]
},
"docker-daemon": {
"": [
{
"type": "insecureAcceptAnything"
}
]
}
}
}
Copy public key:
# Copy cosign public key
sudo cp cosign.pub /etc/containers/cosign.pub
sudo chmod 644 /etc/containers/cosign.pub
Test policy enforcement:
# Try to pull unsigned image (should fail)
buildah pull registry.example.com/unsigned-app:1.0
# Pull signed image (should succeed)
buildah pull registry.example.com/myapp:1.0
Generate SLSA provenance:
# Install slsa-framework
wget https://github.com/slsa-framework/slsa-github-generator/releases/latest/download/slsa-generator-linux-amd64
chmod +x slsa-generator-linux-amd64
sudo mv slsa-generator-linux-amd64 /usr/local/bin/slsa-generator
# Generate provenance
slsa-generator generate --source-uri github.com/example/myapp \
--source-tag v1.0.0 \
--output-file provenance.json
Attach provenance to image:
# Attach provenance attestation
cosign attest --key cosign.key \
--predicate provenance.json \
registry.example.com/myapp:1.0
# Verify attestation
cosign verify-attestation --key cosign.pub \
--type slsaprovenance \
registry.example.com/myapp:1.0
Configure allowed registries:
# /etc/containers/registries.conf
[registries.search]
registries = [
'registry.example.com',
'quay.io',
'docker.io'
]
[registries.insecure]
registries = []
[registries.block]
registries = [
'untrusted-registry.example.com'
]
Configure registry authentication:
# Login to registry
buildah login registry.example.com
# Or use credentials file
# ~/.config/containers/auth.json
{
"auths": {
"registry.example.com": {
"auth": "base64-encoded-credentials"
}
}
}
GitHub Actions example:
# .github/workflows/build.yml
name: Build and Sign Image
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Buildah
run: |
sudo apt-get update
sudo apt-get install -y buildah
- name: Lint Dockerfile
uses: hadolint/hadolint-action@v3
with:
dockerfile: Containerfile
- name: Scan for secrets
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build image
run: buildah bud -t registry.example.com/myapp:${{ github.sha }} .
- name: Sign image
run: |
echo "${{ secrets.COSIGN_KEY }}" > cosign.key
cosign sign --key cosign.key registry.example.com/myapp:${{ github.sha }}
rm cosign.key
- name: Push image
run: buildah push registry.example.com/myapp:${{ github.sha }}
GitLab CI example:
# .gitlab-ci.yml
stages:
- lint
- build
- sign
- push
lint:
stage: lint
image: registry.hub.docker.com/hadolint/hadolint:latest
script:
- hadolint Containerfile
scan-secrets:
stage: lint
image: registry.hub.docker.com/zricethezav/gitleaks:latest
script:
- gitleaks detect --source . --verbose
build:
stage: build
image: registry.hub.docker.com/quay.io/buildah/stable:latest
script:
- buildah bud -t registry.example.com/myapp:$CI_COMMIT_SHA .
- buildah push registry.example.com/myapp:$CI_COMMIT_SHA
sign:
stage: sign
image: registry.hub.docker.com/sigstore/cosign:latest
script:
- echo "$COSIGN_KEY" > cosign.key
- cosign sign --key cosign.key registry.example.com/myapp:$CI_COMMIT_SHA
variables:
COSIGN_KEY: $COSIGN_KEY
Integrate Trivy in pipeline:
# Add to CI/CD pipeline
- name: Scan image
run: |
trivy image --exit-code 1 --severity CRITICAL,HIGH \
registry.example.com/myapp:${{ github.sha }}
- name: Generate SBOM
run: |
trivy image --format spdx-json \
-o sbom.json \
registry.example.com/myapp:${{ github.sha }}
Use buildah with Trivy:
# Build and scan in one step
buildah bud -t myapp .
buildah run myapp -- trivy fs /app
# Or mount trivy in build container
buildah run --mount type=bind,src=/path/to/trivy,dst=/usr/local/bin/trivy \
myapp -- trivy fs /app
Use Kyverno to enforce signed images:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce
background: true
rules:
- name: check-image-signature
match:
resources:
kinds:
- Pod
validate:
message: "Images must be signed"
imageVerify:
image: "*"
attestors:
- count: 1
entries:
- keys:
publicKeys: |
-----BEGIN PUBLIC KEY-----
<cosign public key>
-----END PUBLIC KEY-----
Enable Buildah logging:
# Buildah logs to journal
sudo journalctl -u buildah
# Or configure log file
export BUILDAH_LOG_LEVEL=info
buildah bud -t myapp . 2>&1 | tee /var/log/buildah-build.log
Track build metadata:
# Record build information
buildah inspect myapp | jq '{
created: .Created,
digest: .Digest,
labels: .Labels,
user: .User
}' > build-metadata.json
# Store in artifact repository
cp build-metadata.json /var/artifacts/myapp-build-metadata.json
Monitor for unusual activity:
# Install auditd
sudo apt install auditd
# Add Buildah audit rules
sudo auditctl -w /usr/bin/buildah -k buildah_exec
sudo auditctl -w /var/lib/containers -k buildah_storage
sudo auditctl -w /etc/containers -k buildah_config
# View audit logs
sudo ausearch -k buildah_exec -i
sudo ausearch -k buildah_storage -i
Check for unauthorized builds:
# List recent buildah commands
sudo ausearch -k buildah_exec -i | grep "buildah bud"
# Check for privileged builds
sudo ausearch -k buildah_exec -i | grep "privileged"
| Control | Status | Notes |
|---|---|---|
| Rootless builds enabled | ☐ | User namespaces configured |
| Build host isolated | ☐ | Separate from production |
| Build cache secured | ☐ | Proper permissions, regular cleanup |
| No privileged builds | ☐ | Unless absolutely necessary |
| Dockerfiles linted | ☐ | hadolint in CI/CD |
| Base images pinned | ☐ | By digest, no latest |
| Build context scanned | ☐ | gitleaks/trufflehog |
| Images signed | ☐ | cosign with key or keyless |
| Signature policy enforced | ☐ | policy.json configured |
| Build provenance recorded | ☐ | SLSA attestation |
| Only trusted registries | ☐ | Insecure registries disabled |
| CI/CD secured | ☐ | Secrets protected |
| Images scanned | ☐ | Trivy before push |
| Build activity audited | ☐ | auditd configured |
latest tag - Pin base images by digestCheck Buildah security status:
# Check Buildah version
buildah version
# Check rootless status
buildah info | grep -E "rootless|uid|gid"
# Check storage configuration
buildah info | jq '.Store'
# Check registry configuration
cat /etc/containers/registries.conf | grep -v "^#" | grep -v "^$"
Audit build security:
# Lint Dockerfile
hadolint Containerfile
# Scan for secrets
gitleaks detect --source . --verbose
# Check base image digest
buildah inspect --format '{{.Digest}}' myapp
# Verify image signature
cosign verify --key cosign.pub registry.example.com/myapp:1.0
# Check for privileged operations
grep -E "privileged|SYS_ADMIN|NET_ADMIN" Containerfile