Overview
Container security addresses threats across the entire container lifecycle, from image creation through runtime execution and orchestration. Containers share the host kernel while providing process isolation through Linux namespaces and control groups, creating a unique attack surface distinct from traditional virtualization or bare-metal deployments.
The shared kernel architecture introduces specific vulnerabilities. A container escape allows attackers to break isolation and access the host system or other containers. Unlike virtual machines with hardware-level separation, containers depend entirely on kernel security features for isolation. Kernel vulnerabilities affect all containers on a host simultaneously.
Container images present supply chain risks. Images consist of layers containing application code, dependencies, and operating system packages. Each layer may introduce vulnerabilities through outdated packages, embedded secrets, or malicious code. Images pulled from public registries carry unknown provenance unless cryptographically signed and verified.
Runtime security concerns differ from build-time security. Build-time security focuses on image composition, dependency scanning, and configuration hardening. Runtime security monitors container behavior, enforces resource limits, manages network policies, and detects anomalous activity. Both phases require different tools and strategies.
Orchestration platforms like Kubernetes add complexity. Pod security policies, network segmentation, role-based access control, and secrets management operate at the cluster level. Misconfigurations cascade across multiple containers, amplifying security impact.
# Container configuration exposing security parameters
FROM ruby:3.2-alpine
# Non-root user execution reduces privilege escalation risk
RUN addgroup -g 1000 appgroup && \
adduser -D -u 1000 -G appgroup appuser
# Minimal package installation limits attack surface
RUN apk add --no-cache postgresql-client
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
# Read-only filesystem except explicit volumes
CMD ["bundle", "exec", "rails", "server"]
Container security intersects with application security, infrastructure security, and supply chain security. Applications must handle secrets securely, validate inputs, and follow secure coding practices. Infrastructure must provide network isolation, resource limits, and audit logging. Supply chain security requires dependency verification, image scanning, and provenance tracking.
Key Principles
Least Privilege Execution dictates containers run with minimum required permissions. Default container configurations often run processes as root inside the container namespace. While namespace isolation provides some protection, root processes can exploit kernel vulnerabilities to escape. Running as a non-root user inside containers limits exploitation potential even if the application is compromised.
Capabilities in Linux divide root privileges into distinct units. Traditional containers receive all capabilities, granting excessive permissions. Dropping unnecessary capabilities reduces the attack surface. Most applications require only a subset: CAP_NET_BIND_SERVICE for binding privileged ports, CAP_CHOWN for changing file ownership. Capabilities like CAP_SYS_ADMIN grant extensive system access and should be removed.
Immutable Infrastructure treats containers as disposable units. Configuration changes occur through rebuilding images, not modifying running containers. This prevents configuration drift and ensures reproducibility. Read-only filesystems enforce immutability at runtime, preventing attackers from modifying binaries or installing tools even after compromising the application.
Ephemeral containers store no persistent state internally. Application data belongs in volumes or external storage. Logs stream to centralized systems rather than writing to container filesystems. This separation enables rapid container replacement during security incidents.
Defense in Depth layers multiple security controls. Image scanning detects known vulnerabilities in packages. Runtime monitoring catches exploitation attempts. Network policies limit lateral movement. No single control provides complete protection. Attackers must bypass multiple defenses to achieve their objectives.
Security controls operate at different lifecycle stages:
Build Time:
- Dependency scanning
- Static analysis
- Image signing
- Policy enforcement
Deploy Time:
- Admission control
- Configuration validation
- Resource quotas
- RBAC policies
Runtime:
- Behavioral monitoring
- Network filtering
- System call filtering
- Anomaly detection
Minimal Attack Surface reduces available exploitation paths. Base images contain only essential components. Removing package managers, shells, and debugging tools from production images prevents attackers from using these tools after compromise. Multi-stage builds compile and package applications, then copy only runtime artifacts to minimal base images.
Container images bundle application code with system dependencies. Each additional package increases potential vulnerabilities. Alpine Linux images provide minimal footprints but use musl libc instead of glibc, potentially causing compatibility issues. Distroless images remove even the shell, containing only the application and runtime dependencies.
Secure Defaults prevent common misconfigurations. Containers should deny privileged mode, host network access, and host path mounts by default. Security contexts should enforce non-root execution, read-only filesystems, and restricted capabilities. Default configurations must be secure; security should not require extra steps.
Kubernetes Pod Security Standards codify secure defaults:
- Privileged: No restrictions (not recommended for production)
- Baseline: Prevents common privilege escalations
- Restricted: Enforces hardened configurations
Secrets Isolation protects sensitive data from exposure. Embedding secrets in images persists them across the supply chain. Image layers remain in registries even after updates, exposing historical secrets. Environment variables appear in process listings and logs. Proper secrets management uses encrypted storage, access controls, and rotation policies.
Secrets require different handling than configuration. Configuration can be public; secrets must remain confidential. Configuration can be versioned in source control; secrets need secure storage. Configuration changes infrequently; secrets should rotate regularly.
Security Implications
Image Vulnerabilities represent the most common container security risk. Images contain operating system packages with known CVEs. Scanning tools identify these vulnerabilities by comparing package versions against vulnerability databases. However, scans only detect known vulnerabilities. Zero-day vulnerabilities and application-level bugs require different detection methods.
Vulnerability severity ratings guide remediation priorities. Critical vulnerabilities in network-facing services demand immediate attention. Low-severity vulnerabilities in unused packages may be acceptable risks. Context matters: a critical vulnerability in a database driver matters more if the application uses that driver.
Remediation strategies depend on vulnerability location:
# Gemfile showing dependency vulnerability management
source 'https://rubygems.org'
# Direct dependency with known vulnerability
# Update to patched version
gem 'rails', '~> 7.1.0' # Previously 7.0.x with CVE
# Transitive dependency vulnerability
# May require updating parent gem or explicit override
gem 'nokogiri', '>= 1.15.4' # Force newer version
# Development dependencies pose less risk
group :development, :test do
gem 'rspec-rails'
end
Container Escapes break isolation between containers and hosts. Kernel vulnerabilities allow containerized processes to access host resources. The shared kernel architecture means any kernel exploit affects all containers simultaneously. Mitigation requires kernel patching, restricting capabilities, and using security modules like AppArmor or SELinux.
Privileged containers disable most security features, running with full host access. These containers can mount host filesystems, access all devices, and modify kernel parameters. Applications rarely require privileged mode. When necessary, granting specific capabilities proves safer than privileged execution.
Supply Chain Attacks inject malicious code through dependencies or base images. Attackers compromise popular images or packages, distributing malware to downstream users. Package typosquatting tricks developers into installing malicious gems with similar names to legitimate packages.
Image provenance verification confirms image origins:
# Cosign signature verification in Ruby
require 'open3'
def verify_image_signature(image, public_key_path)
cmd = [
'cosign', 'verify',
'--key', public_key_path,
image
]
stdout, stderr, status = Open3.capture3(*cmd)
unless status.success?
raise "Image signature verification failed: #{stderr}"
end
JSON.parse(stdout)
end
# Verify before deployment
begin
verify_image_signature(
'myregistry.io/myapp:v1.2.3',
'/etc/cosign/public.key'
)
puts "Image signature valid"
rescue => e
abort "Deployment blocked: #{e.message}"
end
Network Exposure creates attack vectors between containers and external systems. Containers default to bridge networking, sharing network namespaces with other containers. Without network policies, any container can connect to any other container. Lateral movement allows attackers to pivot from compromised containers to other services.
Kubernetes Network Policies enforce ingress and egress rules:
- Ingress policies restrict incoming connections by source
- Egress policies limit outbound connections by destination
- Default deny policies block all traffic except explicitly allowed
Secret Exposure leaks credentials through multiple channels. Environment variables appear in process listings visible via /proc. Logs often capture environment variable dumps during debugging. Docker inspect commands reveal environment variables to anyone with Docker socket access. Container orchestrators may store secrets in etcd without encryption at rest.
Secure secrets management requires:
- Encrypted storage at rest
- Encrypted transmission
- Access control limiting which pods can access secrets
- Audit logging of secret access
- Regular rotation schedules
- Automatic revocation on compromise
Resource Exhaustion enables denial of service attacks. Containers without resource limits can consume all host CPU, memory, or storage. A single compromised container can starve all other containers on the host. Resource quotas prevent individual containers from monopolizing resources.
CPU limits prevent containers from consuming all CPU cycles. Memory limits trigger out-of-memory kills instead of exhausting host memory. Storage quotas prevent disk fill attacks. Process limits prevent fork bombs.
Privilege Escalation allows attackers to gain additional permissions. Containers running as root inside namespaces can exploit setuid binaries, kernel vulnerabilities, or misconfigurations to gain host root access. Security contexts prevent privilege escalation by blocking processes from gaining additional capabilities.
# Kubernetes security context in Ruby deployment manifest
require 'yaml'
deployment = {
'apiVersion' => 'apps/v1',
'kind' => 'Deployment',
'metadata' => {
'name' => 'secure-app'
},
'spec' => {
'template' => {
'spec' => {
'securityContext' => {
'runAsNonRoot' => true,
'runAsUser' => 1000,
'fsGroup' => 1000,
'seccompProfile' => {
'type' => 'RuntimeDefault'
}
},
'containers' => [{
'name' => 'app',
'image' => 'myapp:latest',
'securityContext' => {
'allowPrivilegeEscalation' => false,
'readOnlyRootFilesystem' => true,
'capabilities' => {
'drop' => ['ALL']
}
}
}]
}
}
}
}
puts deployment.to_yaml
Implementation Approaches
Static Analysis Security scans container images before deployment. These tools analyze image contents without running containers, identifying vulnerabilities, misconfigurations, and policy violations. Trivy, Grype, and Clair scan image layers for package vulnerabilities. Hadolint checks Dockerfiles against best practices. Checkov validates infrastructure-as-code security policies.
Static scanning integrates into CI/CD pipelines:
1. Build image from Dockerfile
2. Run security scan on built image
3. Check scan results against policy thresholds
4. Block deployment if critical vulnerabilities found
5. Push image to registry only if scan passes
This approach prevents vulnerable images from reaching production but cannot detect runtime behaviors or zero-day exploits.
Runtime Security Monitoring observes container behavior during execution. These systems monitor system calls, network connections, file access, and process execution. Baseline profiles establish normal behavior, then detect deviations indicating potential attacks. Falco, Sysdig, and Aqua Security provide runtime monitoring capabilities.
Runtime monitoring detects:
- Unexpected process execution (shells spawned in production containers)
- Suspicious network connections (outbound connections to unknown IPs)
- File modifications in read-only filesystems
- Privilege escalation attempts
- Container escape attempts
This approach catches attacks that bypass static analysis but introduces runtime overhead and may generate false positives requiring tuning.
Image Signing and Verification establishes trust chains for container images. Signing associates cryptographic signatures with images, proving authenticity and integrity. Verification checks signatures before deployment, ensuring images match signed versions and originate from trusted sources.
Content trust workflow:
Build Stage:
1. Build container image
2. Generate cryptographic hash of image
3. Sign hash with private key
4. Store signature in registry metadata
Deploy Stage:
1. Pull image from registry
2. Retrieve associated signature
3. Verify signature with public key
4. Compare image hash with signed hash
5. Deploy only if verification succeeds
Docker Content Trust and Sigstore Cosign implement image signing. Admission controllers enforce signature verification in Kubernetes, blocking unsigned or invalidly signed images.
Least Privilege Configuration minimizes permissions granted to containers. This strategy configures security contexts, capabilities, and access controls to grant only required permissions. Implementation requires understanding application requirements and mapping them to specific capabilities.
Progressive privilege reduction:
1. Start with default container configuration
2. Run application and identify required capabilities
3. Remove capabilities not used by application
4. Test application with reduced capabilities
5. Enable read-only filesystem where possible
6. Configure non-root user execution
7. Drop additional capabilities if tests pass
8. Document minimum required permissions
This approach requires application-specific analysis and testing but produces maximally restricted containers.
Network Segmentation isolates containers using network policies. Microservices architectures create multiple containers requiring controlled communication. Network segmentation prevents compromised containers from accessing other services.
Segmentation strategies:
Namespace Isolation: Deploy different applications in separate Kubernetes namespaces with default deny network policies.
Service Mesh: Implement mutual TLS between services using Istio or Linkerd, encrypting all inter-service communication.
Zero Trust Networking: Require authentication and authorization for every connection regardless of network location.
# Network policy generator in Ruby
class NetworkPolicyGenerator
def initialize(app_name, namespace)
@app_name = app_name
@namespace = namespace
end
def default_deny
{
'apiVersion' => 'networking.k8s.io/v1',
'kind' => 'NetworkPolicy',
'metadata' => {
'name' => "default-deny-#{@namespace}",
'namespace' => @namespace
},
'spec' => {
'podSelector' => {},
'policyTypes' => ['Ingress', 'Egress']
}
}
end
def allow_from_namespace(source_namespace)
{
'apiVersion' => 'networking.k8s.io/v1',
'kind' => 'NetworkPolicy',
'metadata' => {
'name' => "allow-from-#{source_namespace}",
'namespace' => @namespace
},
'spec' => {
'podSelector' => {
'matchLabels' => {'app' => @app_name}
},
'policyTypes' => ['Ingress'],
'ingress' => [{
'from' => [{
'namespaceSelector' => {
'matchLabels' => {'name' => source_namespace}
}
}]
}]
}
}
end
end
generator = NetworkPolicyGenerator.new('api', 'production')
puts generator.default_deny.to_yaml
puts generator.allow_from_namespace('frontend').to_yaml
Ruby Implementation
Ruby applications running in containers require specific security considerations. The Ruby runtime, gem dependencies, and Rails framework introduce distinct security concerns beyond general container security.
Dependency Management in Ruby uses Bundler to manage gems. The Gemfile.lock pins exact versions, ensuring reproducible builds. However, locked versions may contain vulnerabilities. Regular dependency updates and vulnerability scanning prevent exploitation of known gem vulnerabilities.
# Gemfile with security-conscious dependency management
source 'https://rubygems.org'
ruby '~> 3.2.0'
# Web framework with security patches
gem 'rails', '~> 7.1.0'
# Database adapter with version constraints
gem 'pg', '~> 1.5'
# Background job processing
gem 'sidekiq', '~> 7.1'
# Security-specific gems
gem 'rack-attack' # Rate limiting and blocking
gem 'secure_headers' # Security header management
gem 'brakeman', group: :development # Static security analysis
group :development, :test do
gem 'bundler-audit' # Gem vulnerability scanning
gem 'rspec-rails'
end
Scanning for gem vulnerabilities uses bundler-audit:
# Rakefile task for vulnerability scanning
require 'bundler/audit/task'
Bundler::Audit::Task.new
namespace :security do
desc 'Update vulnerability database and audit gems'
task :scan do
Bundler::Audit::Task.new.update
Bundler::Audit::Task.new.check
end
end
Container Image Construction for Ruby applications follows multi-stage build patterns. Build stage compiles native extensions and installs development dependencies. Production stage copies only runtime requirements, reducing image size and attack surface.
# Multi-stage Ruby container build
FROM ruby:3.2-alpine AS builder
RUN apk add --no-cache \
build-base \
postgresql-dev \
nodejs \
yarn
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install --jobs 4
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --production
COPY . .
RUN bundle exec rails assets:precompile
# Production stage
FROM ruby:3.2-alpine
RUN apk add --no-cache \
postgresql-client \
tzdata
RUN addgroup -g 1000 rails && \
adduser -D -u 1000 -G rails rails
WORKDIR /app
COPY --from=builder --chown=rails:rails /usr/local/bundle /usr/local/bundle
COPY --from=builder --chown=rails:rails /app /app
USER rails
EXPOSE 3000
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
Secrets Management in Ruby applications handles database credentials, API keys, and encryption keys. Rails credentials system encrypts secrets at rest using a master key. The master key must remain outside the container image.
# config/initializers/secret_management.rb
class SecretManager
def self.database_url
if ENV['KUBERNETES_SERVICE_HOST']
# Running in Kubernetes - read from mounted secret
File.read('/var/secrets/database_url').strip
else
# Development environment
ENV.fetch('DATABASE_URL')
end
rescue Errno::ENOENT => e
Rails.logger.error("Failed to read database secret: #{e.message}")
raise "Database configuration unavailable"
end
def self.api_key(service)
# Try environment variable first
env_key = "#{service.upcase}_API_KEY"
return ENV[env_key] if ENV[env_key]
# Fall back to secrets file in Kubernetes
secret_path = "/var/secrets/#{service}_api_key"
File.read(secret_path).strip if File.exist?(secret_path)
end
end
# config/database.yml
production:
url: <%= SecretManager.database_url %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
Health Checks verify application readiness and liveness. Kubernetes probes determine when to route traffic to containers and when to restart failed containers. Ruby applications implement health check endpoints exposing application state.
# app/controllers/health_controller.rb
class HealthController < ApplicationController
skip_before_action :verify_authenticity_token
def liveness
# Simple liveness check - process is running
render json: {status: 'ok'}, status: :ok
end
def readiness
# Readiness check - can serve requests
checks = {
database: check_database,
redis: check_redis,
storage: check_storage
}
if checks.values.all?
render json: {status: 'ready', checks: checks}, status: :ok
else
render json: {status: 'not_ready', checks: checks},
status: :service_unavailable
end
end
private
def check_database
ActiveRecord::Base.connection.active?
rescue
false
end
def check_redis
Redis.current.ping == 'PONG'
rescue
false
end
def check_storage
ActiveStorage::Blob.count >= 0
true
rescue
false
end
end
# config/routes.rb
Rails.application.routes.draw do
get '/health/liveness', to: 'health#liveness'
get '/health/readiness', to: 'health#readiness'
end
Signal Handling ensures graceful shutdown when containers stop. Kubernetes sends SIGTERM before forcefully killing containers. Ruby applications should trap signals and complete in-flight requests before exiting.
# config/puma.rb
workers ENV.fetch("WEB_CONCURRENCY", 2)
threads_count = ENV.fetch("RAILS_MAX_THREADS", 5)
threads threads_count, threads_count
port ENV.fetch("PORT", 3000)
environment ENV.fetch("RAILS_ENV", "production")
on_worker_boot do
ActiveRecord::Base.establish_connection
end
# Graceful shutdown handling
on_worker_shutdown do
puts "Worker shutting down gracefully..."
ActiveRecord::Base.connection_pool.disconnect!
end
# Wait for requests to complete before shutdown
worker_shutdown_timeout 30
# Health check endpoint for Kubernetes probes
activate_control_app 'tcp://0.0.0.0:9293'
Tools & Ecosystem
Image Scanning Tools analyze container images for vulnerabilities, misconfigurations, and policy violations. Each tool offers different features, databases, and integration options.
Trivy scans images, filesystems, and Git repositories for vulnerabilities and misconfigurations. It supports multiple package managers including Bundler for Ruby gems, NPM for JavaScript, and operating system packages. Trivy downloads vulnerability databases on demand and updates them automatically.
# Ruby wrapper for Trivy scanning
require 'json'
require 'open3'
class TrivyScanner
def initialize(image)
@image = image
end
def scan(severity: ['CRITICAL', 'HIGH'])
cmd = [
'trivy', 'image',
'--format', 'json',
'--severity', severity.join(','),
'--no-progress',
@image
]
stdout, stderr, status = Open3.capture3(*cmd)
unless status.success?
raise "Trivy scan failed: #{stderr}"
end
parse_results(JSON.parse(stdout))
end
private
def parse_results(data)
results = data['Results'] || []
vulnerabilities = results.flat_map do |result|
(result['Vulnerabilities'] || []).map do |vuln|
{
package: vuln['PkgName'],
installed_version: vuln['InstalledVersion'],
fixed_version: vuln['FixedVersion'],
severity: vuln['Severity'],
title: vuln['Title'],
description: vuln['Description']
}
end
end
{
total: vulnerabilities.count,
by_severity: vulnerabilities.group_by { |v| v[:severity] }
.transform_values(&:count),
vulnerabilities: vulnerabilities
}
end
end
# Usage in CI pipeline
scanner = TrivyScanner.new('myapp:latest')
results = scanner.scan(severity: ['CRITICAL'])
if results[:total] > 0
puts "Found #{results[:total]} critical vulnerabilities"
results[:vulnerabilities].each do |vuln|
puts "#{vuln[:package]}: #{vuln[:title]}"
end
exit 1
end
Grype specializes in vulnerability scanning with high accuracy and low false positives. It matches packages against multiple vulnerability databases and provides detailed remediation guidance.
Falco provides runtime security monitoring using eBPF or kernel modules. It detects suspicious behavior like shell execution in containers, unauthorized file access, and unexpected network connections. Falco rules define security policies written in YAML.
# Falco rule detecting shell spawning in containers
- rule: Shell Spawned in Container
desc: Detect shell process spawned in container
condition: >
spawned_process and
container and
shell_procs and
proc.pname exists and
not user_expected_shell_spawn_processes
output: >
Shell spawned in container
(user=%user.name container_id=%container.id
image=%container.image.repository
process=%proc.cmdline parent=%proc.pname)
priority: WARNING
tags: [container, shell, mitre_execution]
Cosign signs and verifies container images using cryptographic signatures. It integrates with Sigstore for keyless signing using OpenID Connect identity tokens. Sigstore transparency logs provide tamper-proof audit trails of all signatures.
# Ruby interface for Cosign operations
class CosignManager
def sign_image(image, key_path)
cmd = ['cosign', 'sign', '--key', key_path, image]
stdout, stderr, status = Open3.capture3(*cmd)
unless status.success?
raise "Image signing failed: #{stderr}"
end
stdout
end
def verify_image(image, public_key_path)
cmd = ['cosign', 'verify', '--key', public_key_path, image]
stdout, stderr, status = Open3.capture3(*cmd)
unless status.success?
raise "Verification failed: #{stderr}"
end
JSON.parse(stdout)
end
def verify_keyless(image, issuer, subject)
cmd = [
'cosign', 'verify',
'--certificate-identity', subject,
'--certificate-oidc-issuer', issuer,
image
]
stdout, stderr, status = Open3.capture3(*cmd)
unless status.success?
raise "Keyless verification failed: #{stderr}"
end
JSON.parse(stdout)
end
end
Open Policy Agent (OPA) enforces policies across Kubernetes clusters. Gatekeeper extends OPA for Kubernetes admission control, validating resources against security policies before deployment. Policies written in Rego language define constraints.
# Ruby client for OPA policy evaluation
require 'net/http'
require 'json'
class OPAPolicyClient
def initialize(opa_url)
@opa_url = URI(opa_url)
end
def evaluate_policy(policy_path, input)
uri = URI.join(@opa_url, "/v1/data/#{policy_path}")
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = {input: input}.to_json
response = Net::HTTP.start(uri.hostname, uri.port) do |http|
http.request(request)
end
result = JSON.parse(response.body)
{
allowed: result.dig('result', 'allow'),
violations: result.dig('result', 'violations') || []
}
end
end
# Check if deployment meets security policies
client = OPAPolicyClient.new('http://opa:8181')
deployment_spec = {
'spec' => {
'template' => {
'spec' => {
'containers' => [{
'name' => 'app',
'image' => 'myapp:latest',
'securityContext' => {
'runAsNonRoot' => false # Policy violation
}
}]
}
}
}
}
result = client.evaluate_policy('kubernetes/admission', deployment_spec)
unless result[:allowed]
puts "Policy violations detected:"
result[:violations].each { |v| puts "- #{v}" }
exit 1
end
Harbor provides enterprise container registry with built-in security scanning, image signing, and access control. It integrates with vulnerability scanners and enforces deployment policies based on scan results.
Aqua Security and Sysdig Secure offer commercial platforms combining image scanning, runtime protection, and compliance monitoring. These platforms provide centralized security management across containerized environments.
Docker Bench Security audits Docker host configurations against CIS Docker Benchmark recommendations. It checks for security misconfigurations in Docker daemon, images, and container runtime.
Common Pitfalls
Running Containers as Root represents the most frequent security mistake. Default container configurations execute processes as root user inside the container namespace. While namespaces provide isolation, root processes can exploit kernel vulnerabilities or misconfigurations to escape.
Many developers assume namespace isolation provides sufficient protection. However, root processes inside containers possess capabilities enabling privilege escalation. CVE-2019-5736 allowed root processes in containers to overwrite the host runc binary, achieving container escape.
# Insecure Dockerfile - runs as root
FROM ruby:3.2
WORKDIR /app
COPY . .
RUN bundle install
CMD ["rails", "server"] # Runs as root (UID 0)
# Secure Dockerfile - creates and uses non-root user
FROM ruby:3.2
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
RUN bundle install
USER appuser # Explicitly switch to non-root user
CMD ["rails", "server"]
Embedding Secrets in Images permanently exposes credentials. Environment variables passed during image build persist in image layers. Anyone with image access can extract historical build arguments from layer metadata.
# INSECURE - secret persists in image layer
FROM ruby:3.2
ARG DATABASE_PASSWORD # Build-time argument
ENV DATABASE_PASSWORD=${DATABASE_PASSWORD} # Now in image layer
# Even if removed later, secret remains in layer history
# SECURE - mount secrets at runtime
FROM ruby:3.2
# No secrets in image
CMD ["rails", "server"]
# Kubernetes deployment mounts secret
# apiVersion: v1
# kind: Pod
# spec:
# containers:
# - name: app
# env:
# - name: DATABASE_PASSWORD
# valueFrom:
# secretKeyRef:
# name: db-credentials
# key: password
Git repositories containing Dockerfiles with secrets create additional exposure. Removing secrets from the latest commit does not remove them from history. Secret scanning tools like git-secrets or truffleHog detect historical secrets in repositories.
Ignoring Image Layer Caching leads to stale dependencies. Docker caches layers during builds. If package installation commands remain unchanged, Docker reuses cached layers even when upstream package versions update with security patches.
# Problematic caching
FROM ruby:3.2
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install # Cached if Gemfile unchanged
COPY . .
# Forces cache invalidation for updates
FROM ruby:3.2
WORKDIR /app
# Copy dependency files
COPY Gemfile Gemfile.lock ./
# Add cache-busting for security updates
ARG CACHE_BUST=1
RUN bundle update --conservative || bundle install
COPY . .
Excessive Capabilities grant unnecessary permissions. Default container capabilities include CAP_CHOWN, CAP_NET_BIND_SERVICE, CAP_SETUID, and others. Most applications require only a subset. CAP_SYS_ADMIN grants extensive system access and should never be used.
# Kubernetes security context dropping all capabilities
security_context = {
'capabilities' => {
'drop' => ['ALL'],
'add' => ['NET_BIND_SERVICE'] # Only if binding port < 1024
},
'allowPrivilegeEscalation' => false
}
Unrestricted Network Access enables lateral movement. Default network policies allow all inter-pod communication. Compromised containers can scan and exploit other services. Zero-trust networking requires explicit allow rules for each communication path.
Missing Resource Limits permits resource exhaustion. Containers without CPU and memory limits can monopolize host resources. A single memory leak or CPU-intensive process affects all containers on the host.
# Container resource limits in Kubernetes
resources = {
'limits' => {
'cpu' => '1000m', # 1 CPU core maximum
'memory' => '512Mi' # 512 MB maximum
},
'requests' => {
'cpu' => '100m', # Guaranteed 0.1 CPU
'memory' => '128Mi' # Guaranteed 128 MB
}
}
Trusting Public Images introduces supply chain risks. Images from Docker Hub or other public registries lack verification. Attackers compromise popular images, injecting malware or backdoors. Using official images reduces risk but does not eliminate it.
Base images should come from trusted sources with signature verification. Copying images to private registries and scanning before use provides additional control.
Ignoring Logging and Monitoring prevents incident detection. Containers without centralized logging lose logs when they terminate. Security events disappear with ephemeral containers. Structured logging to external systems enables security monitoring and forensic analysis.
# config/environments/production.rb
Rails.application.configure do
# JSON logging for centralized log aggregation
config.log_formatter = Logger::Formatter.new
# Semantic logger for structured logging
config.semantic_logger.application = 'myapp'
config.semantic_logger.add_appender(
io: STDOUT,
formatter: :json
)
# Log security events
config.after_initialize do
ActiveSupport::Notifications.subscribe('security.authentication_failed') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
Rails.logger.warn(
message: 'Authentication failed',
ip: event.payload[:ip],
username: event.payload[:username],
timestamp: event.time
)
end
end
end
Reference
Container Security Checklist
| Security Control | Implementation | Verification |
|---|---|---|
| Non-root user | USER directive in Dockerfile | docker inspect | grep User |
| Read-only filesystem | readOnlyRootFilesystem: true | Security context in pod spec |
| Dropped capabilities | Drop ALL, add only required | Security context capabilities field |
| No privilege escalation | allowPrivilegeEscalation: false | Security context setting |
| Resource limits | CPU and memory limits/requests | Pod resource configuration |
| Image scanning | Scan before deployment | CI/CD pipeline integration |
| Image signing | Cosign or Notary v2 | Admission controller verification |
| Network policies | Default deny with explicit allows | kubectl get networkpolicy |
| Secrets management | Mounted volumes, not env vars | Check pod environment |
| Security context | Pod and container contexts | kubectl describe pod |
Vulnerability Scanning Tools Comparison
| Tool | Languages | Databases | Output Formats | Integration |
|---|---|---|---|---|
| Trivy | Multi-language | NVD, Red Hat, Debian | JSON, SARIF, table | CI/CD, registries |
| Grype | Multi-language | NVD, GitHub Advisory | JSON, table, CycloneDX | CLI, containers |
| Clair | OS packages | Multiple distros | JSON | Registry integration |
| Snyk | Application deps | Proprietary | JSON, HTML | IDE, CI/CD, SCM |
| Anchore | OS, application | NVD, vendor feeds | JSON | Kubernetes, CI/CD |
Common Container Capabilities
| Capability | Purpose | Risk Level | Typical Usage |
|---|---|---|---|
| CAP_CHOWN | Change file ownership | Low | File permission management |
| CAP_NET_BIND_SERVICE | Bind ports below 1024 | Low | Web servers on port 80/443 |
| CAP_SETUID | Change process UID | Medium | Service user switching |
| CAP_SETGID | Change process GID | Medium | Group permission changes |
| CAP_NET_ADMIN | Network configuration | High | Network utilities, VPNs |
| CAP_SYS_ADMIN | System administration | Critical | Almost never needed |
| CAP_SYS_PTRACE | Process tracing | High | Debugging tools only |
Image Security Best Practices
| Practice | Rationale | Implementation |
|---|---|---|
| Multi-stage builds | Reduce attack surface | Separate builder and runtime stages |
| Minimal base images | Fewer vulnerabilities | Use Alpine or distroless images |
| Dependency pinning | Reproducible builds | Lock file versions (Gemfile.lock) |
| Layer optimization | Efficient caching | Order commands by change frequency |
| No secrets in images | Prevent credential exposure | Use runtime secret injection |
| Regular rebuilds | Update dependencies | Automated build schedules |
| Signature verification | Supply chain security | Cosign or Notary integration |
| Vulnerability scanning | Detect known CVEs | Scan in CI/CD pipeline |
Network Policy Types
| Policy Type | Purpose | Example Rule |
|---|---|---|
| Default Deny Ingress | Block all incoming | policyTypes: Ingress with empty rules |
| Default Deny Egress | Block all outgoing | policyTypes: Egress with empty rules |
| Namespace Isolation | Separate environments | namespaceSelector matchLabels |
| Pod Selector | Target specific pods | podSelector matchLabels |
| Port Restrictions | Limit exposed ports | ports protocol and port number |
| CIDR-based | External IP control | ipBlock with CIDR notation |
Security Context Fields
| Field | Scope | Purpose | Example Value |
|---|---|---|---|
| runAsNonRoot | Container/Pod | Enforce non-root execution | true |
| runAsUser | Container/Pod | Specify user ID | 1000 |
| runAsGroup | Container/Pod | Specify group ID | 1000 |
| fsGroup | Pod | Volume ownership group | 2000 |
| readOnlyRootFilesystem | Container | Prevent filesystem writes | true |
| allowPrivilegeEscalation | Container | Block privilege gains | false |
| capabilities.drop | Container | Remove capabilities | ALL |
| capabilities.add | Container | Grant specific capabilities | NET_BIND_SERVICE |
| seccompProfile | Container/Pod | System call filtering | RuntimeDefault |
| seLinuxOptions | Container/Pod | SELinux context | type: container_t |
Ruby Gem Security Tools
| Gem | Purpose | Usage |
|---|---|---|
| bundler-audit | Scan for vulnerable gems | bundle audit check --update |
| brakeman | Static analysis for Rails | brakeman -q -z |
| secure_headers | Security header management | SecureHeaders::Configuration.default |
| rack-attack | Rate limiting and blocking | Rack::Attack.throttle |
| devise | Authentication framework | Built-in security features |
| pundit | Authorization framework | Policy-based access control |