DevSecOps

Dagger CI/CD Security Benefits

How Dagger's containerized pipeline model improves CI/CD security with hermetic builds, portability, and reduced platform dependency.

James
Platform Security Engineer
6 min read

Dagger takes a fundamentally different approach to CI/CD. Instead of writing YAML configurations for a specific platform, you write pipelines as code in Go, Python, or TypeScript. Each pipeline step runs in a container. The same pipeline runs identically on your laptop, in GitHub Actions, in GitLab CI, or anywhere else that has a container runtime.

From a security perspective, this architectural decision has significant implications — mostly positive, but with new considerations that traditional CI/CD platforms do not present.

The Security Case for Containerized Pipelines

Traditional CI/CD platforms have a shared infrastructure problem. Your builds run on shared runners with shared caches, shared network access, and shared filesystem state. The isolation between builds depends on the platform's implementation, which varies from solid (ephemeral VMs) to minimal (shared Docker daemons).

Dagger changes this equation. Every pipeline step runs in its own container, managed by BuildKit. Steps communicate through explicit data dependencies, not shared filesystems. The pipeline's execution graph is determined by your code, not by the platform's interpretation of YAML.

Hermetic Builds

A Dagger pipeline explicitly declares its inputs. If a step needs a file, a dependency, or a secret, it must be passed in programmatically:

func (m *MyPipeline) Build(ctx context.Context, source *Directory) *Container {
    return dag.Container().
        From("golang:1.21").
        WithDirectory("/src", source).
        WithExec([]string{"go", "build", "-o", "/app", "."})
}

The build container starts from a known base image, receives the source directory as an explicit input, and produces an output. There is no ambient environment to leak secrets from, no cache from a previous job to poison, and no shared filesystem to tamper with.

This hermeticity is not absolute — containers can still make network requests, and BuildKit has its own caching layer — but it is dramatically better than traditional CI/CD environments.

Platform Independence Reduces Lock-In Risk

When your pipeline is coupled to a specific platform (GitHub Actions, GitLab CI, Jenkins), your security posture is tied to that platform's security model. If CircleCI is breached, your CircleCI pipelines are compromised. If GitHub Actions has a vulnerability, your GitHub workflows are affected.

Dagger pipelines are platform-agnostic. Your pipeline code runs the same way everywhere. This means:

  • You can migrate between platforms without rewriting security controls.
  • Your security scanning, secret management, and artifact signing logic is portable.
  • A platform-specific vulnerability does not necessarily compromise your pipeline logic — only the thin wrapper that triggers Dagger.
# GitHub Actions just triggers Dagger
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run Dagger Pipeline
        uses: dagger/dagger-for-github@v5
        with:
          verb: call
          args: build --source=.

The GitHub Actions workflow is minimal. The actual pipeline logic — including security checks — lives in your Dagger module.

Secret Management in Dagger

Dagger has a first-class Secret type that prevents accidental exposure:

func (m *MyPipeline) Deploy(
    ctx context.Context,
    source *Directory,
    registryPassword *Secret,
) error {
    _, err := dag.Container().
        From("docker:latest").
        WithSecretVariable("REGISTRY_PASSWORD", registryPassword).
        WithExec([]string{"docker", "login", "-u", "deploy", "-p", "$REGISTRY_PASSWORD"}).
        Sync(ctx)
    return err
}

Key properties of Dagger secrets:

  • Secrets are never included in the container's filesystem layer.
  • Secrets are not visible in build logs or cache keys.
  • Secrets are passed through a dedicated channel, not environment variable inheritance.
  • The Dagger engine scrubs secrets from output.

This is meaningfully better than the env-var-based secret injection most CI/CD platforms use, where secrets can be accidentally logged, cached, or leaked through process environment inspection.

Supply Chain Security Integration

Because Dagger pipelines are code, you can integrate supply chain security tooling directly:

func (m *MyPipeline) SecureBuild(ctx context.Context, source *Directory) *Container {
    // Scan dependencies
    scanned := dag.Container().
        From("aquasec/trivy:latest").
        WithDirectory("/src", source).
        WithExec([]string{"trivy", "fs", "--exit-code", "1", "--severity", "HIGH,CRITICAL", "/src"})

    // Build after scan passes
    built := dag.Container().
        From("golang:1.21").
        WithDirectory("/src", source).
        WithExec([]string{"go", "build", "-o", "/app", "."})

    // Sign the resulting image
    signed := dag.Container().
        From("gcr.io/projectsigstore/cosign:latest").
        WithExec([]string{"cosign", "sign", "--key", "cosign.key", "registry.example.com/app:latest"})

    return built
}

The dependency scanning, building, and signing steps are explicit code with explicit data flow. If the scan fails, the build does not proceed. There is no YAML configuration to misconfigure or bypass.

Caching Security

BuildKit's caching layer is powerful but has security implications:

  • Cache layers may contain sensitive data if not handled carefully. Use Dagger's Secret type instead of baking secrets into container layers.
  • Shared caches between Dagger runs on the same machine can be poisoned. Use separate BuildKit instances for different trust levels.
  • Remote caching (pushing cache to a registry) requires securing the registry and trusting the cache source.
// Cache dependencies separately from application code
func (m *MyPipeline) Build(ctx context.Context, source *Directory) *Container {
    goCache := dag.CacheVolume("go-mod-cache")
    
    return dag.Container().
        From("golang:1.21").
        WithMountedCache("/go/pkg/mod", goCache).
        WithDirectory("/src", source).
        WithExec([]string{"go", "build", "-o", "/app", "."})
}

Cache volumes are isolated per Dagger module by default, reducing cross-project contamination.

Testing Pipeline Security Locally

One of Dagger's strongest security benefits is local testability. You can run your entire pipeline on your development machine:

dagger call build --source=.
dagger call security-scan --source=.
dagger call deploy --source=. --registry-password=env:REGISTRY_PASSWORD

This means:

  • Security engineers can review and test pipeline changes before they reach CI.
  • Pipeline changes can be validated in isolation without pushing to a shared CI environment.
  • Security scanning configurations can be tuned locally without burning CI minutes.

Limitations to Consider

Dagger is not a silver bullet:

  • BuildKit daemon access: Dagger requires a BuildKit daemon. On shared CI runners, this may mean a shared daemon with shared caches.
  • Container escapes: While rare, container escapes in BuildKit would compromise the pipeline. Keep BuildKit updated.
  • Network access: Containers in Dagger can make arbitrary network requests by default. Network policies depend on the host environment.
  • Module trust: Dagger modules from external sources carry supply chain risks similar to GitHub Actions or npm packages. Vet before use.

How Safeguard.sh Helps

Safeguard.sh integrates with Dagger pipelines to provide security monitoring that spans the pipeline's entire lifecycle. It tracks which Dagger modules your pipelines depend on, monitors for vulnerabilities in base images used by pipeline steps, and ingests SBOM and vulnerability scan results regardless of which CI/CD platform triggers the Dagger run. Because Dagger pipelines produce consistent artifacts across platforms, Safeguard.sh can enforce consistent security policies — ensuring the same standards apply whether a pipeline runs locally, in GitHub Actions, or in your self-hosted infrastructure.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.