Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions src/pages/explanations/security/dependency-proxy-security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,19 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "@document-writing-tools

## Overview

The DevGuard Dependency Proxy is a security-focused intermediary that sits between your development environment and public package registries. When you request a package from npm, Go, or PyPI, the request goes through DevGuard's proxy, which performs security checks and caching before delivering the package to you.
The DevGuard Dependency Proxy is a security-focused intermediary that sits between your development environment and public package registries. When you request a package from one of the supported registries, the request goes through DevGuard's proxy, which performs security checks and caching before delivering the package to you.

## Supported Ecosystems

DevGuard currently proxies three major package ecosystems:
DevGuard currently proxies seven package ecosystems:

- **npm** (Node.js packages from registry.npmjs.org)
- **Go** (Go modules from proxy.golang.org)
- **PyPI** (Python packages from pypi.org)
- **Maven** (Java packages from Maven Central)
- **Composer** (PHP packages from Packagist)
- **Debian** (apt packages from deb.debian.org)
- **OCI** (container images from public registries)

## The Request Flow

Expand All @@ -53,8 +57,11 @@ If the package is flagged as malicious:
- The incident is logged and metrics are updated
- Any existing cache of the package is purged

### 4. Cache Lookup
If the package passes the security check, DevGuard looks in its local cache. The cache system uses different time-to-live (TTL) values based on content type:
### 4. Package Rules
Next, the [package rules](/how-to-guides/dependency-proxy/package-rules) of the organization, project or repository are evaluated against the package — for example its name, version, release time and, for npm, its publisher. The last matching rule decides whether the package is allowed or denied. Denied packages are rejected with `403 Forbidden` and the name of the rule; for metadata requests, denied versions are removed from the version list instead.

### 5. Cache Lookup
If the package passes these checks, DevGuard looks in its local cache. The cache system uses different time-to-live (TTL) values based on content type:

**For npm:**
- Package tarballs are cached for 24 hours
Expand All @@ -68,13 +75,13 @@ If the package passes the security check, DevGuard looks in its local cache. The
- Distribution files (wheels and source) are cached for 7 days
- Package metadata is cached for 1 hour

### 5. Integrity Verification
### 6. Integrity Verification
When serving from cache, DevGuard verifies the integrity of the cached data using SHA256 checksums. Each cached file has an associated `.sha256` file containing its hash. If verification fails, the corrupted cache is discarded and the package is refetched.

### 6. Upstream Fetch
### 7. Upstream Fetch
If there's a cache miss or the cache is expired, DevGuard fetches the package from the upstream registry. During this fetch, it forwards appropriate headers to ensure compatibility with registry requirements.

### 7. Version Resolution (npm-specific)
### 8. Version Resolution (npm-specific)
For npm metadata requests that don't specify a version, DevGuard performs an additional security step:
- It fetches the package metadata
- Extracts the "latest" version from the `dist-tags`
Expand All @@ -83,10 +90,10 @@ For npm metadata requests that don't specify a version, DevGuard performs an add

This prevents scenarios where a developer runs `npm install package` (without version) and unknowingly gets a malicious latest version.

### 8. Cache Storage
### 9. Cache Storage
Successful responses from upstream are stored in the cache along with their SHA256 integrity hash. The cache directory structure mirrors the package path for easy organization and cleanup.

### 9. Response Delivery
### 10. Response Delivery
Finally, DevGuard returns the package to the client with additional headers indicating:
- Cache status (`X-Cache: HIT` or `X-Cache: MISS`)
- Proxy type (`X-Proxy-Type: npm/go/pypi`)
Expand Down
1 change: 1 addition & 0 deletions src/pages/how-to-guides/dependency-proxy/_meta.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export default {
index: { title: 'Overview' },
'package-rules': { title: 'Package Rules' },
'cache-management': { title: 'Cache Management' },
'setup-go-proxy': { title: 'Setup Go Proxy' },
'setup-npm-proxy': { title: 'Setup NPM Proxy' },
Expand Down
2 changes: 1 addition & 1 deletion src/pages/how-to-guides/dependency-proxy/ci-runners.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The proxy URLs in the runner configuration contain a dependency proxy secret:
| Go modules | `GOPROXY` | `https://<your-devguard-url>/api/v1/dependency-proxy/<secret>/go/` |
| PyPI | `PIP_INDEX_URL` | `https://<your-devguard-url>/api/v1/dependency-proxy/<secret>/pypi/simple/` |

The secret links the requests to your organization, project or repository in DevGuard, so the proxy applies the rules and the [minimum package age](/how-to-guides/dependency-proxy#minimum-package-age) configured there. Requests without a secret are only checked against the malicious package database.
The secret links the requests to your organization, project or repository in DevGuard, so the proxy applies the [package rules](/how-to-guides/dependency-proxy/package-rules) configured there. Requests without a secret are only checked against the malicious package database.

<Callout type="warning">
Treat the secret like a credential. Store the runner configuration in a Kubernetes secret or a sealed/encrypted secret instead of committing it in plain text.
Expand Down
30 changes: 16 additions & 14 deletions src/pages/how-to-guides/dependency-proxy/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ DevGuard includes a built-in dependency proxy that acts as a protective layer be
1. **Request Interception**: When you install a package, the proxy intercepts the request
2. **Database Check**: The package is checked against the [OSV dataset](https://osv.dev), which includes GitHub malware advisories — kept current via the [vulndb sync](/reference/vulnerability-database/update-schedule)
3. **Blocking**: If flagged as malicious, the request is blocked and logged
4. **Caching**: Safe packages are cached with SHA256 integrity verification
5. **Metrics**: All blocked attempts are recorded in Prometheus metrics
4. **Package Rules**: Your [package rules](/how-to-guides/dependency-proxy/package-rules) decide whether the package is allowed, e.g. based on its release age
5. **Caching**: Safe packages are cached with SHA256 integrity verification
6. **Metrics**: All blocked attempts are recorded in Prometheus metrics

## Supported Ecosystems

Expand All @@ -49,21 +50,22 @@ For setup instructions, see the ecosystem-specific guides:

To route all CI jobs through the proxy at once, see [Self-Hosted CI Runners](/how-to-guides/dependency-proxy/ci-runners).

## Minimum Package Age
## Package Rules

The proxy can be configured to refuse any package version published more recently than a specified number of hours. This gives the security community time to detect and flag malicious releases before they reach your teams — a simple but effective defense against attacks that rely on packages being downloaded within hours of publication.
Package rules allow or deny packages with [CEL](https://github.com/google/cel-spec) expressions. They are configured per organization, project or repository and apply to requests using its proxy URL, which contains a secret. Rules are evaluated top to bottom and the last matching rule wins.

The minimum age is configured per organization, project or repository and applies to requests using its proxy URL, which contains a secret. Versions that are too new are hidden from the package metadata, so version ranges resolve to the newest version that is old enough; a download of a too new version is rejected with `403 Forbidden`.
A minimum release age is the most common rule. It refuses versions published more recently than a given time, which gives the security community time to detect and flag malicious releases before they reach your teams:

| Ecosystem | Minimum package age |
|-----------|---------------------|
| npm | Supported |
| PyPI | Supported |
| Debian (apt) | Supported — too new versions are rejected but not hidden, see [Setup Debian Proxy](/how-to-guides/dependency-proxy/setup-debian-proxy#blocked-packages-and-signed-package-lists) |
| Go modules | Supported |
| Maven | Supported |
| Composer (Packagist) | Supported |
| OCI (container images) | Not supported — registries expose no reliable publish date, see [Setup OCI Proxy](/how-to-guides/dependency-proxy/setup-oci-proxy) |
```yaml
rules:
- name: min-release-age
action: deny
expr: pkg.ecosystem != "oci" && now() - timestamp(pkg.releasedAt) < duration("120h")
```

Versions that are blocked are hidden from the package metadata, so version ranges resolve to an allowed version; a download of a blocked version is rejected with `403 Forbidden`. Debian package lists are signed and cannot be filtered, see [Setup Debian Proxy](/how-to-guides/dependency-proxy/setup-debian-proxy#blocked-packages-and-signed-package-lists). OCI registries expose no reliable publish date, so the release age cannot be checked for container images.

See [Package Rules](/how-to-guides/dependency-proxy/package-rules) for the available fields, functions and more examples.

## Cache & Security

Expand Down
180 changes: 180 additions & 0 deletions src/pages/how-to-guides/dependency-proxy/package-rules.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
---
title: Dependency Proxy Package Rules
description: "Allow or deny packages in the DevGuard dependency proxy with CEL rules — minimum release age, blocked scopes, publisher changes and exceptions."
seo:
robots: index,follow
og:
image: /og-image.png
type: article
schema:
type: TechArticle
keyword_primary: dependency proxy package rules
lang: en-US
ignoreChecks: null
---

import { Callout } from '@document-writing-tools/kernux-theme'

# Dependency Proxy Package Rules

Package rules decide which packages the [dependency proxy](/how-to-guides/dependency-proxy) serves. Each rule is a
[CEL (Common Expression Language)](https://github.com/google/cel-spec) expression that is evaluated against the
requested package. Rules are configured per organization, project or repository in the **Dependency Proxy**
settings and apply to requests using the proxy URL of that scope.

The malicious package check always runs, independently of your rules — an `allow` rule can never let a known
malicious package through.

## Format

Rules are written in YAML:

```yaml
rules:
- name: min-release-age
action: deny
expr: pkg.ecosystem != "oci" && now() - timestamp(pkg.releasedAt) < duration("120h")
- name: allow-trusted
action: allow
expr: pkg.name == "my-trusted-package"
```

| Field | Description |
|-------|-------------|
| `name` | Shown in the error message when the rule blocks a package. |
| `action` | `deny` blocks a matching package, `allow` lets it through. |
| `expr` | A CEL expression that evaluates to a boolean. `true` means the rule matches. |

## Evaluation

- Rules are evaluated **top to bottom** and the **last matching rule wins**. Put general rules first and exceptions below them.
- A package that matches no rule is **allowed**.
- A configuration with invalid YAML, an unknown `action` or an expression that does not compile is rejected when saving.
- If an expression fails at runtime — for example because it reads a field the package does not have — the package is **blocked** (fail closed).

A blocked download is answered with `403 Forbidden`, the header `X-Not-Allowed-Package: blocked` and the name of the rule.
Blocked versions are also hidden from the version lists, so version ranges resolve to an allowed version. npm, PyPI
and Composer check every version; Go and Maven check from the newest version downwards until the first allowed one,
because each check needs an extra request for the release time. Debian package lists are signed and cannot be filtered, see
[Setup Debian Proxy](/how-to-guides/dependency-proxy/setup-debian-proxy#blocked-packages-and-signed-package-lists).

## The `pkg` variable

| Field | Type | Description |
|-------|------|-------------|
| `pkg.name` | string | Package name, e.g. `lodash`, `@babel/core`, `github.com/gin-gonic/gin` |
| `pkg.version` | string | Requested version |
| `pkg.ecosystem` | string | `npm`, `go`, `pypi`, `maven`, `composer`, `deb` or `oci` |
| `pkg.purl` | string | e.g. `pkg:npm/lodash@4.17.21`; OCI uses the image reference, e.g. `docker.io/library/nginx:latest` |
| `pkg.registry` | string | Upstream registry, e.g. `https://pypi.org` |
| `pkg.releasedAt` | string (RFC 3339) | Publish time of the version. Use `timestamp(pkg.releasedAt)` to compare it. |
| `pkg.publisher.name` | string | User who published this version (npm only) |
| `pkg.publisher.changedSinceLastVersion` | bool | The publisher differs from the one of the previously published version (npm only) |

Not every ecosystem provides every field:

| Ecosystem | Source of `releasedAt` | `publisher` |
|-----------|------------------------|-------------|
| npm | `time` field of the package metadata | ✓ |
| Go modules | `Time` field of the `.info` file | – |
| PyPI | Upload time of the file | – |
| Composer | `time` field reported by Packagist | – |
| Maven | `Last-Modified` header from Maven Central | – |
| Debian | `Last-Modified` header from the mirror | – |
| OCI | not available | – |

### Missing fields

Fields that are not available are absent, not empty. Reading an absent field makes the expression fail, which
blocks the package. Use `has()` to decide explicitly what happens:

```yaml
# allow packages without a release time
expr: has(pkg.releasedAt) && now() - timestamp(pkg.releasedAt) < duration("120h")

# block packages without a release time
expr: '!has(pkg.releasedAt) || now() - timestamp(pkg.releasedAt) < duration("120h")'
```

Expressions that start with `!` must be quoted, otherwise YAML reads the `!` as a tag.

<Callout type="warning">
OCI images never have a release time. A minimum age rule without `has()` or an `pkg.ecosystem != "oci"` check
blocks every image.
</Callout>

## Functions

| Function | Example |
|----------|---------|
| `now()` | Current time as timestamp |
| `timestamp(string)` | `timestamp(pkg.releasedAt)` |
| `duration(string)` | `duration("120h")` — hours, minutes or seconds (`h`, `m`, `s`) |
| `has(field)` | `has(pkg.publisher)` |
| `startsWith`, `endsWith`, `contains`, `matches` | `pkg.name.startsWith("@evil-scope/")`, `pkg.name.matches("^left-?pad$")` |
| `in` | `pkg.ecosystem in ["npm", "pypi"]` |

All standard CEL operators (`==`, `!=`, `<`, `&&`, `||`, `!`) are available.

## Cookbook

### Minimum release age

```yaml
- name: min-release-age
action: deny
expr: pkg.ecosystem != "oci" && now() - timestamp(pkg.releasedAt) < duration("120h")
```

### Exception for one package

Place it **below** the rule it overrides.

```yaml
- name: allow-lodash
action: allow
expr: pkg.name == "lodash"
```

### Block a whole npm scope

```yaml
- name: block-evil-scope
action: deny
expr: pkg.name.startsWith("@evil-scope/")
```

### Block one exact version

```yaml
- name: block-lodash-4-17-21
action: deny
expr: pkg.purl == "pkg:npm/lodash@4.17.21"
```

### Block versions published by a new maintainer

Account takeovers often publish a new version from a different account.

```yaml
- name: publisher-changed
action: deny
expr: has(pkg.publisher) && pkg.publisher.changedSinceLastVersion
```

### Allow only one ecosystem

```yaml
- name: deny-all
action: deny
expr: "true"
- name: allow-npm
action: allow
expr: pkg.ecosystem == "npm"
```

## Related Documentation

- [Dependency Proxy Overview](/how-to-guides/dependency-proxy) — how the proxy works
- [Dependency Proxy Security](/explanations/security/dependency-proxy-security) — where rules fit into the request flow
- [CEL Expression Reference for VEX Rules](/reference/vex-cel-reference) — CEL in VEX rules
Loading
Loading