CrackedRuby CrackedRuby

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