Tutorial

Docker Security with Hadolint

A complete hands-on tutorial for learning Hadolint and Dockerfile security best practices.

This tutorial walks you through Dockerfile security and best practices using Hadolint. Follow each section in order and run the commands in the playground terminal.

Introduction to Hadolint

What is Hadolint?

Hadolint is a powerful Dockerfile linter written in Haskell that helps you write better, more secure, and more efficient Docker images. It analyzes your Dockerfile and provides recommendations based on best practices.

Key Features

  • Static Analysis: Analyzes Dockerfile syntax and structure without building the image
  • Bash Validation: Validates inline bash scripts using ShellCheck integration
  • Best Practices: Enforces Docker best practices and security guidelines
  • Rule-Based: Provides specific error codes (DLxxxx) for each issue found
  • CI/CD Integration: Easy to integrate into CI/CD pipelines
  • Multiple Output Formats: Supports JSON, TAP, and other output formats

Why Use Hadolint?

  1. Security: Identifies security vulnerabilities before deployment
  2. Performance: Suggests optimizations to reduce image size and build time
  3. Maintainability: Ensures consistent Dockerfile patterns across projects
  4. Quality: Catches common mistakes and anti-patterns early
  5. Documentation: Provides clear explanations for each issue found

Installation

This is the easiest method and works on any system with Docker installed.

docker pull hadolint/hadolint

To use hadolint with Docker:

docker run --rm -i hadolint/hadolint < Dockerfile

Or create an alias for convenience:

echo 'alias hadolint="docker run --rm -i hadolint/hadolint"' >> ~/.bashrc
source ~/.bashrc

Option 2: Download Binary

Download the latest binary from GitHub Releases

Linux/Ubuntu:

sudo wget -O /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
sudo chmod +x /usr/local/bin/hadolint

macOS:

wget -O /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Darwin-x86_64
chmod +x /usr/local/bin/hadolint

Verify Installation

hadolint --version

Note: If using Docker, verify with:

docker run --rm hadolint/hadolint --version

Basic Usage

Command Line Syntax

hadolint [OPTIONS] Dockerfile

Common Options

  • --ignore RULE: Ignore specific rules (e.g., --ignore DL3008)
  • --format FORMAT: Output format (tty, json, checkstyle, codeclimate, gitlab_codeclimate, sarif)
  • --failure-threshold LEVEL: Exit with failure code if issues >= LEVEL (error, warning, info, style, ignore)
  • --no-color: Disable colored output
  • --verbose: Enable verbose output
  • --config FILE: Path to configuration file

Basic Example

Step 1: Create a simple Dockerfile to test:

vi Dockerfile

Step 2: Add some basic Dockerfile content, save and exit (:wq).

Step 3: Test with various options:

# Lint a Dockerfile
hadolint Dockerfile

# Lint with JSON output
hadolint --format json Dockerfile

# Ignore specific rules
hadolint --ignore DL3008 --ignore DL3009 Dockerfile

Basic Dockerfile Analysis

Exercise 1: Dockerfile Without Issues

Let's start with a well-written Dockerfile that follows best practices.

Example: Dockerfile.good

Step 1: Create the Dockerfile:

vi Dockerfile

Step 2: Paste the following Dockerfile content:

# Use specific version instead of latest
FROM node:18-alpine AS builder

# Set working directory
WORKDIR /app

# Install dependencies first (better layer caching)
COPY package*.json ./
RUN npm ci --only=production

# Copy application code
COPY . .

# Build the application
RUN npm run build

# Production stage
FROM node:18-alpine

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# Set working directory
WORKDIR /app

# Copy only necessary files from builder
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./

# Switch to non-root user
USER nodejs

# Expose port
EXPOSE 3000

# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js

# Use exec form for CMD
CMD ["node", "dist/index.js"]

Step 3: Save and exit (:wq), then run Hadolint:

hadolint Dockerfile

Expected Output: No issues found! ✅

If using the good Dockerfile example, Hadolint should return no errors or warnings.

Why This Dockerfile is Good

  1. Specific Base Image: Uses node:18-alpine instead of latest (DL3006)
  2. Multi-stage Build: Reduces final image size
  3. Layer Caching: Dependencies installed before copying code
  4. Non-root User: Runs as nodejs user instead of root (DL3002)
  5. Health Check: Includes health check for container monitoring (DL3018)
  6. Exec Form CMD: Uses exec form ["node", "dist/index.js"] instead of shell form (DL3025)
  7. Proper Ownership: Sets file ownership with --chown (DL3027)

Exercise 2: Dockerfile With Common Issues

Now let's examine a Dockerfile with common issues.

Example: Dockerfile.bad

Step 1: Create the Dockerfile:

vi Dockerfile.bad

Step 2: Paste the following Dockerfile content:

FROM ubuntu:latest

RUN apt-get update
RUN apt-get install -y python3 python3-pip
RUN pip3 install flask requests

WORKDIR /app
COPY . .

RUN chmod +x start.sh
RUN ./start.sh

EXPOSE 8080

CMD python3 app.py

Step 3: Save and exit (:wq), then run Hadolint:

hadolint Dockerfile.bad

Actual Output: Multiple issues will be reported! ❌

Dockerfile.bad:1 DL3007 warning: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
Dockerfile.bad:3 DL3009 info: Delete the apt lists (/var/lib/apt/lists) after installing something
Dockerfile.bad:4 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
Dockerfile.bad:4 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
Dockerfile.bad:4 DL3015 info: Avoid additional packages by specifying `--no-install-recommends`
Dockerfile.bad:5 DL3013 warning: Pin versions in pip. Instead of `pip install <package>` use `pip install <package>==<version>` or `pip install --requirement <requirements file>`
Dockerfile.bad:5 DL3042 warning: Avoid use of cache directory with pip. Use `pip install --no-cache-dir <package>`
Dockerfile.bad:5 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
Dockerfile.bad:11 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
Dockerfile.bad:15 DL3025 warning: Use arguments JSON notation for CMD and ENTRYPOINT arguments

Common Issues You'll See

  1. DL3007: Using latest tag is prone to errors - pin the version explicitly
  2. DL3008: Pin versions in apt-get install
  3. DL3009: Delete the apt-get lists after installing
  4. DL3013: Pin versions in pip install
  5. DL3015: Avoid additional packages with --no-install-recommends
  6. DL3025: Use arguments JSON notation for CMD
  7. DL3042: Avoid use of cache directory with pip
  8. DL3059: Multiple consecutive RUN instructions - consider consolidation

Understanding Hadolint Output

Hadolint provides clear, actionable feedback with severity levels:

  • warning: Issues that should be addressed
  • info: Suggestions for improvement

Each line shows:

  • File: The Dockerfile being analyzed
  • Line Number: Where the issue occurs
  • Rule Code: The specific rule (DLxxxx)
  • Severity: warning or info
  • Description: What the issue is and how to fix it

Hands-On Lab

Use the interactive playground to analyze the Dockerfiles yourself. The playground provides an Ubuntu environment with Docker and Hadolint pre-installed.

Fixing Common Issues

Issue-by-Issue Fixes

Let's go through each common issue and learn how to fix it.

Issue 1: DL3006 - Avoid using latest tag

Problem:

FROM ubuntu:latest

Solution:

FROM ubuntu:22.04

Explanation: Using latest tag makes builds unpredictable. Different builds might use different versions, leading to inconsistent behavior. Always pin to a specific version.

Issue 2: DL3008 - Pin versions in apt-get install

Problem:

RUN apt-get install -y python3 python3-pip

Better Solution:

# Use specific versions when possible, or at least use --no-install-recommends
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    python3 \
    python3-pip && \
    rm -rf /var/lib/apt/lists/*

Explanation: Pinning versions ensures reproducible builds. However, in practice, you might want to use --no-install-recommends to reduce image size and combine commands to reduce layers.

Issue 3: DL3009 - Delete the apt-get lists after installing

Problem:

RUN apt-get update
RUN apt-get install -y python3 python3-pip

Solution:

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    python3 \
    python3-pip && \
    rm -rf /var/lib/apt/lists/*

Explanation: The apt cache takes up unnecessary space. Removing it reduces image size significantly.

Issue 4: DL3013 - Pin versions in pip install

Problem:

RUN pip3 install flask requests

Better Solution:

# Use requirements.txt for better maintainability
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt

Explanation: Pinning versions ensures reproducible builds. Using a requirements.txt file is more maintainable for multiple packages.

Issue 5: DL3002 - Last USER should not be root

Problem: The Dockerfile runs as root by default.

Solution:

# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser

# ... other instructions ...

# Switch to non-root user before CMD
USER appuser

Explanation: Running containers as root is a security risk. If the container is compromised, an attacker would have root access.

Issue 6: DL3025 - Use arguments JSON notation for CMD

Problem:

CMD python3 app.py

Solution:

CMD ["python3", "app.py"]

Explanation: The exec form (JSON notation) is preferred because:

  • It doesn't spawn a shell process (more efficient)
  • Signals are properly handled (important for graceful shutdowns)
  • More predictable behavior

Issue 7: DL3045 - COPY to a relative destination without WORKDIR

Problem: Copying to a relative path without setting WORKDIR can be ambiguous.

Solution:

# Bad: relative destination without WORKDIR
COPY app/ ./app/

# Good: set WORKDIR first
WORKDIR /app
COPY app/ ./

Explanation: Setting WORKDIR ensures all relative paths are deterministic and easier to reason about.

Fixed Dockerfile

Step 1: Create the fixed Dockerfile:

vi Dockerfile.fixed

Step 2: Paste the following fixed Dockerfile content:

# Use specific version
FROM ubuntu:22.04

# Install dependencies in a single layer and clean up
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    python3 \
    python3-pip && \
    rm -rf /var/lib/apt/lists/*

# Install Python packages with pinned versions
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt

# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Set working directory
WORKDIR /app

# Copy application code
COPY --chown=appuser:appuser . .

# Switch to non-root user
USER appuser

# Expose port
EXPOSE 8080

# Use exec form for CMD
CMD ["python3", "app.py"]

Step 3: Save and exit (:wq), then run Hadolint:

hadolint Dockerfile.fixed

Actual Output: Significantly fewer issues! Most critical problems are fixed, but some warnings may remain:

Dockerfile.fixed:5 DL3008 warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
Dockerfile.fixed:12 DL3045 warning: `COPY` to a relative destination without `WORKDIR` set.

Note: The fixed version resolves most critical security issues (like running as root, using latest tag, etc.). The remaining warnings are about version pinning (which can be strict in practice) and WORKDIR usage. These are less critical but still good to address for production use.

Hands-On Lab

Use the interactive playground to practice fixing Dockerfile issues. The playground provides an Ubuntu environment with Docker and Hadolint pre-installed, so you can test your fixes in real-time.

Practice Exercise

  1. Review the Dockerfile.bad example above
  2. Run Hadolint on it to see all issues
  3. Compare with the Dockerfile.fixed example
  4. Try fixing the issues yourself before looking at the solution

Advanced Patterns

Multi-stage Builds

Multi-stage builds are a powerful technique to reduce final image size by excluding build tools and dependencies from the production image.

Example: Multi-stage Build with Issues

Step 1: Create the Dockerfile:

vi Dockerfile-multi-issues

Step 2: Paste the following Dockerfile content:

FROM node:latest AS builder
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build

FROM node:latest
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD node dist/index.js

Step 3: Save and exit (:wq), then run Hadolint:

hadolint Dockerfile-multi-issues

Actual Output: Multiple issues will be reported! ❌

Dockerfile-multi-issues:1 DL3007 warning: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
Dockerfile-multi-issues:5 DL3059 info: Multiple consecutive `RUN` instructions. Consider consolidation.
Dockerfile-multi-issues:7 DL3007 warning: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
Dockerfile-multi-issues:11 DL3025 warning: Use arguments JSON notation for CMD and ENTRYPOINT arguments

Issues in This Dockerfile

  1. Using latest tag (DL3007) - appears twice (builder and production stages)
  2. Multiple consecutive RUN instructions (DL3059) - should be consolidated
  3. Copying everything before installing dependencies (poor layer caching)
  4. Copying node_modules instead of installing production dependencies
  5. Running as root (DL3002) - not explicitly shown but default behavior
  6. Using shell form for CMD (DL3025)

Fixed Multi-stage Build

Step 1: Create the fixed Dockerfile:

vi Dockerfile-multi-fixed

Step 2: Paste the following fixed Dockerfile content:

FROM node:18-alpine AS builder
WORKDIR /app

# Copy package files first for better caching
COPY package*.json ./
RUN npm ci

# Copy source and build
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine
WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# Install only production dependencies
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Copy built application
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist

# Switch to non-root user
USER nodejs

EXPOSE 3000

# Use exec form
CMD ["node", "dist/index.js"]

Step 3: Save and exit (:wq), then run Hadolint:

hadolint Dockerfile-multi-fixed

Actual Output: No issues found! ✅

root@ubuntu-01:laborant# hadolint Dockerfile-multi-fixed 
root@ubuntu-01:laborant# 

Perfect! The fixed version passes all Hadolint checks.

Key Improvements

  1. Specific Versions: Uses node:18-alpine instead of latest (fixes DL3007)
  2. Better Caching: Copies package*.json before source code
  3. Production Dependencies: Only installs production dependencies in final stage
  4. Non-root User: Runs as nodejs user (fixes DL3002)
  5. Exec Form: Uses proper JSON notation for CMD (fixes DL3025)
  6. Consolidated RUN: Combined related commands to reduce layers (fixes DL3059)

Layer Caching Optimization

Bad: Poor Layer Ordering

Step 1: Create the Dockerfile:

vi Dockerfile-bad-cache

Step 2: Paste the following content:

COPY . .
RUN npm install

Step 3: Save and exit (:wq).

Problem: Every code change invalidates the npm install cache.

Good: Optimized Layer Ordering

Step 1: Create the Dockerfile:

vi Dockerfile-good-cache

Step 2: Paste the following content:

COPY package*.json ./
RUN npm install
COPY . .

Step 3: Save and exit (:wq).

Why: Dependencies change less frequently than source code. This order allows Docker to cache the dependency layer.

Test both to see the difference:

hadolint Dockerfile-bad-cache
hadolint Dockerfile-good-cache

Combining RUN Commands

Bad: Multiple Layers

Step 1: Create the Dockerfile:

vi Dockerfile-multiple-run

Step 2: Paste the following content:

RUN apt-get update
RUN apt-get install -y python3
RUN rm -rf /var/lib/apt/lists/*

Step 3: Save and exit (:wq), then test:

hadolint Dockerfile-multiple-run

Problem: Creates 3 separate layers, increasing image size. You'll see warnings about multiple consecutive RUN instructions (DL3059).

Good: Single Layer

Step 1: Create the Dockerfile:

vi Dockerfile-consolidated-run

Step 2: Paste the following content:

RUN apt-get update && \
    apt-get install -y --no-install-recommends python3 && \
    rm -rf /var/lib/apt/lists/*

Step 3: Save and exit (:wq), then test:

hadolint Dockerfile-consolidated-run

Why: Each RUN creates a new layer. Combining reduces layers and image size.

This should have fewer or no warnings about multiple RUN instructions.

Using .dockerignore

Important: Create a .dockerignore file before creating your Dockerfile. This file tells Docker which files and directories to exclude from the build context.

Step 1: Create the .dockerignore file first:

vi .dockerignore

Add the following content:

node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.nyc_output
coverage
.vscode

Save and exit (:wq).

Step 2: Then create your Dockerfile:

vi Dockerfile

Add your Dockerfile content, save and exit (:wq).

What happens after adding .dockerignore:

  1. Faster Builds: Docker won't send excluded files to the Docker daemon, reducing build context size
  2. Smaller Images: Excluded files won't be copied into the image layers
  3. Better Security: Sensitive files (like .env) won't accidentally be included
  4. Reduced Build Time: Less data to transfer means faster builds

Example: Without .dockerignore, a build might send 500MB of node_modules to Docker. With .dockerignore, only your source code (maybe 5MB) is sent, making builds much faster.

Test it: After creating both files, build your image and notice the difference:

docker build -t myapp .
docker images myapp

Health Checks

Step 1: Create the Dockerfile:

vi Dockerfile-with-healthcheck

Step 2: Paste the following content with health check:

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

Step 3: Save and exit (:wq), then test:

hadolint Dockerfile-with-healthcheck

Why: Allows Docker and orchestrators to monitor container health.

Hands-On Lab

Use the interactive playground to practice creating optimized multi-stage Dockerfiles. The playground provides an Ubuntu environment with Docker and Hadolint pre-installed for hands-on experimentation.

Testing Your Dockerfiles

Create a test script to verify your Dockerfiles:

vi test-dockerfiles.sh

Add the following content:

#!/bin/bash
echo "Testing Dockerfile.good..."
hadolint Dockerfile.good

echo "Testing Dockerfile.bad..."
hadolint Dockerfile.bad

echo "Testing Dockerfile.fixed..."
hadolint Dockerfile.fixed

Save and exit (:wq), then make it executable and run:

chmod +x test-dockerfiles.sh
./test-dockerfiles.sh

This script will:

  • Test Dockerfile.good (should pass)
  • Test Dockerfile.bad (should have issues)
  • Test Dockerfile.fixed (should pass)

Integration & Configuration

Configuration File

Create a .hadolint.yaml file to customize which rules to ignore or override.

Step 1: Create the configuration file:

vi .hadolint.yaml

Step 2: Add the configuration content:

# Ignore specific rules (these won't be reported)
ignored:
  - DL3008  # Pin versions in apt-get install (can be too strict for some use cases)
  - DL3013  # Pin versions in pip install (can be too strict for some use cases)

# Override rule severity
# You can change the severity level of specific rules
override:
  error: DL3002  # Treat DL3002 (Last USER should not be root) as error instead of warning
  warning: DL3006  # Treat DL3006 (Always tag version) as warning instead of error

# Trusted registries (for DL3017 rule)
trustedRegistries:
  - docker.io
  - gcr.io
  - quay.io

Save and exit (:wq).

Using the Configuration

After creating the configuration file, use it with Hadolint:

hadolint --config .hadolint.yaml Dockerfile

The configuration will apply the ignored rules and severity overrides you specified.

CI/CD Integration

GitHub Actions

Create the GitHub Actions workflow file:

mkdir -p .github/workflows
vi .github/workflows/hadolint.yml

Add the workflow content:

name: Lint Dockerfile

on:
  pull_request:
    paths:
      - 'Dockerfile*'
      - '.dockerfilelintrc'

jobs:
  hadolint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run Hadolint
        uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: Dockerfile
          failure-threshold: warning
          ignore: DL3008,DL3013

Save and exit (:wq).

Pre-commit Hook

Create the pre-commit configuration file:

vi .pre-commit-config.yaml

Add the pre-commit configuration:

repos:
  - repo: https://github.com/hadolint/hadolint
    rev: v2.12.0
    hooks:
      - id: hadolint-docker
        args: ['--ignore', 'DL3008']

Save and exit (:wq).

Install pre-commit:

pip install pre-commit
pre-commit install

GitLab CI

Create or edit the GitLab CI configuration:

vi .gitlab-ci.yml

Add the hadolint job:

hadolint:
  image: hadolint/hadolint:latest
  script:
    - hadolint --ignore DL3008 --ignore DL3013 Dockerfile
  only:
    - merge_requests

Save and exit (:wq).

Makefile

Create or edit the Makefile:

vi Makefile

Add the lint targets:

.PHONY: lint
lint:
    docker run --rm -i hadolint/hadolint < Dockerfile

.PHONY: lint-json
lint-json:
    docker run --rm -i hadolint/hadolint --format json < Dockerfile

Save and exit (:wq).

Output Formats

Hadolint supports multiple output formats. First, create a Dockerfile to test:

vi Dockerfile

Add content, save and exit (:wq), then test different output formats:

# Default (TTY)
hadolint Dockerfile

# JSON
hadolint --format json Dockerfile

# Checkstyle
hadolint --format checkstyle Dockerfile

# Code Climate
hadolint --format codeclimate Dockerfile

# GitLab Code Climate
hadolint --format gitlab_codeclimate Dockerfile

# SARIF
hadolint --format sarif Dockerfile

Failure Thresholds

Control when Hadolint exits with a failure code. Test with your Dockerfile:

# Fail on errors only (default)
hadolint --failure-threshold error Dockerfile

# Fail on warnings and errors
hadolint --failure-threshold warning Dockerfile

# Fail on info, warnings, and errors
hadolint --failure-threshold info Dockerfile

This is useful in CI/CD pipelines where you want to control build failures based on issue severity.

Common Rules Reference

Security Rules

RuleDescriptionSolution
DL3002Last USER should not be rootCreate and switch to a non-root user
DL3006Always tag the version of an image explicitlyUse specific tags like ubuntu:22.04 instead of latest
DL3008Pin versions in apt-get installUse apt-get install package=version
DL3013Pin versions in pip installUse pip install package==version
DL3015Avoid additional packagesUse --no-install-recommends flag

Best Practice Rules

RuleDescriptionSolution
DL3003Use WORKDIR instead of RUN cdUse WORKDIR /path instead of RUN cd /path
DL3009Delete apt-get lists after installingAdd rm -rf /var/lib/apt/lists/* after apt-get install
DL3025Use arguments JSON notation for CMDUse CMD ["executable", "arg1", "arg2"]
DL3027Do not use COPY as rootUse COPY --chown=user:group

Additional Resources

Summary

Hadolint is an essential tool for maintaining high-quality Dockerfiles. By following its recommendations:

  • Security: Reduce attack surface by avoiding root users and pinning versions
  • Performance: Optimize layer caching and reduce image size
  • Reliability: Ensure reproducible builds with version pinning
  • Maintainability: Follow consistent patterns across projects

Remember: Hadolint provides recommendations, but you should understand the reasoning behind each rule and apply them appropriately to your use case.

Happy Linting! 🐳

About the Author

Ajay Kumar

Ajay Kumar

The DevSecOps Ledger is a technical resource dedicated to the integration of security into the modern software development lifecycle. Our goal is to empower engineers with the documentation and practical skills needed to automate security.

Find this author online

More tutorials you might like

Native SSH Access with Pomerium (cover image)

Native SSH Access with Pomerium

Pomerium can be used as a native SSH reverse proxy, adding OAuth authentication and flexible Pomerium policy enforcement to standard SSH connections, without the need for tunnels, or custom clients or servers.

Native SSH Reverse Tunneling with Pomerium (cover image)

Native SSH Reverse Tunneling with Pomerium

Use Pomerium's native SSH support to publish a local service through a standard reverse SSH tunnel, with OpenID Connect (OIDC) authentication and continuous authorization on every request. Reach services behind Network Address Translation (NAT) without firewall holes or custom agents, and control both who can use the service and who can open the tunnel. Application traffic stays on infrastructure you control.

Secure Machine-to-Machine Access with mTLS and Pomerium (cover image)

Secure Machine-to-Machine Access with mTLS and Pomerium

Run a GitHub Actions-compatible continuous integration (CI) job on a private runner and protect its internal API call with mutual TLS (mTLS) and Pomerium. Build separate server and client trust chains, authorize one machine certificate by fingerprint, then revoke, restore, and rotate its credentials through live policy changes.

Harden Access to OpenClaw with Pomerium (cover image)

Harden Access to OpenClaw with Pomerium

Put OpenClaw, a self-hosted AI assistant with shell and file access, behind a web route and an SSH route, both gated by the same identity and Pomerium's context-aware policy. OpenClaw runs in trusted-proxy mode, trusting signed identity headers instead of its own login, while Pomerium's native SSH proxy signs short-lived certificates for shell access.

Learn by doing, not just by reading or watching

Sign up for a free account to start a VM playground right on this page, track your progress, and get notified about new learning materials.

Sign up for free