Skip to content
Merged
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
20 changes: 15 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -265,15 +265,25 @@ _build-with-docker: # Internal target for Docker-based cross-compilation
exit 1; \
fi

docker-image: build-ui ## Build Docker image
docker-image: build-ui ## Build Docker image (LOCAL=1 to use Dockerfile.local)
@echo "$(GREEN)Building Docker image...$(NC)"
$(eval GIT_SHA=$(shell git rev-parse --short HEAD))
@docker build -f transports/Dockerfile -t bifrost -t bifrost:$(GIT_SHA) -t bifrost:latest .
@echo "$(GREEN)Docker image built: bifrost, bifrost:$(GIT_SHA), bifrost:latest$(NC)"
$(eval DOCKERFILE=$(if $(LOCAL),transports/Dockerfile.local,transports/Dockerfile))
@docker build -f $(DOCKERFILE) -t bifrost -t bifrost:$(GIT_SHA) -t bifrost:latest .
@echo "$(GREEN)Docker image built: bifrost, bifrost:$(GIT_SHA), bifrost:latest (using $(DOCKERFILE))$(NC)"

docker-run: ## Run Docker container
docker-run: ## Run Docker container (Usage: make docker-run [CONFIG=path/to/config.json or path/to/dir/])
@echo "$(GREEN)Running Docker container...$(NC)"
@docker run -e APP_PORT=$(PORT) -e APP_HOST=0.0.0.0 -p $(PORT):$(PORT) -e LOG_LEVEL=$(LOG_LEVEL) -e LOG_STYLE=$(LOG_STYLE) -v $(shell pwd):/app/data bifrost
@CONFIG_PATH="$(abspath $(CONFIG))"; \
if [ -n "$(CONFIG)" ]; then \
if [ -d "$$CONFIG_PATH" ]; then \
CONFIG_PATH="$$CONFIG_PATH/config.json"; \
fi; \
CONFIG_MOUNT="-v $$CONFIG_PATH:/app/data/config.json"; \
else \
CONFIG_MOUNT=""; \
fi; \
docker run -e APP_PORT=$(PORT) -e APP_HOST=0.0.0.0 -p $(PORT):$(PORT) -e LOG_LEVEL=$(LOG_LEVEL) -e LOG_STYLE=$(LOG_STYLE) -v $(shell pwd):/app/data $$CONFIG_MOUNT bifrost

docs: ## Prepare local docs
@echo "$(GREEN)Preparing local docs...$(NC)"
Expand Down
Empty file added config.json
Empty file.
7 changes: 7 additions & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,13 @@
}
]
},
{
"tab": "Security",
"icon": "shield",
"pages": [
"security"
]
},
{
"tab": "Benchmarks",
"icon": "chart-line",
Expand Down
Binary file added docs/media/security/codeowners.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/security/codeql.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/security/dep-pinning.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/security/hardned-base-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/security/scout-image-score.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/media/security/step-security.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
289 changes: 289 additions & 0 deletions docs/security.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
---
title: "Security at Bifrost"
description: "Overview of security practices across Bifrost's CI/CD pipelines, container images, supply chain, and deployment infrastructure."
icon: "shield"
sidebarTitle: "Security"
---

Bifrost applies defense-in-depth across its open-source and enterprise repositories. Every pull request,
dependency update, and container image goes through multiple layers of automated security checks before
reaching production.

| Domain | Tool / Practice | Coverage |
| --- | --- | --- |
| Dependency Scanning | Snyk Open Source | Go, Node, Python — all projects |
| SAST | Snyk Code, CodeQL | Full codebase static analysis |
| Container Scanning | Docker Scout | Docker Hub auto-scan on push |
| Artifact Scanning | GCP Artifact Registry | Enterprise container images |
| Dependency Updates | Dependabot | gomod, npm, Docker, GitHub Actions |
| Supply Chain | SHA pinning, npm provenance | 100 % of GitHub Actions (OSS) |
| Container Hardening | FIPS base image, non-root user | Production Dockerfile |
| Security Hardening | StepSecurity, CODEOWNERS | Workflow hardening, code review gates |
| Network Security | Tailscale VPN | Enterprise deployments |

---

## Vulnerability Scanning — Snyk

Bifrost runs two Snyk scanning jobs on every push and pull request. Results are uploaded as
[SARIF](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning)
to the GitHub Security tab.

<Tabs>
<Tab title="Dependency Scanning (Open Source)">
The **Snyk Open Source** job scans all Go, Node, and Python dependencies for known vulnerabilities.

```yaml
snyk test --all-projects --detection-depth=4 --sarif-file-output=snyk.sarif
```

- Scans every module across `core/`, `framework/`, `transports/`, `plugins/`, `ui/`, and `tests/`
- Detection depth of 4 catches transitive dependencies
- Snyk CLI pinned to `v1.1303.2`

</Tab>

<Tab title="SAST (Snyk Code)">
The **Snyk Code** job performs Static Application Security Testing on the full codebase.
A complete build is performed first (Go + Node + Python) so Snyk can analyze compiled artifacts.

```yaml
snyk code test --sarif-file-output=snyk-code.sarif
```

- Detects injection flaws, hardcoded secrets, insecure crypto, and other code-level vulnerabilities
- Builds the full project before scanning for accurate analysis
</Tab>
</Tabs>

<Note>
Snyk checks can be skipped by including `--skip-pipeline` in the first line of a commit message. This is
intended for documentation-only or CI configuration changes.
</Note>

---

## Container Image Security

### Dockerfile Hardening

Production containers follow a strict hardening checklist:

<CardGroup cols={2}>
<Card title="Multi-stage builds" icon="layer-group">
Three stages — UI builder (Node), Go builder, and minimal Alpine runtime — ensure no build tools or
source code leak into the final image.
</Card>
<Card title="FIPS-compliant base image" icon="lock">
Production images use a FIPS 140-2 validated Alpine base image with compliant OpenSSL.
</Card>
<Card title="Non-root execution" icon="user-shield">
The FIPS base image includes a dedicated `appuser`. The container runs as this unprivileged user —
never as root.
</Card>
<Card title="Binary stripping" icon="minimize">
Go binaries are compiled with `-ldflags="-w -s"` to strip debug symbols and DWARF information,
reducing attack surface and image size.
</Card>
</CardGroup>

Additional hardening measures:

- **Static builds** — Compiled with `-tags "sqlite_static"` and `-extldflags '-static'` for fully static linking
- **Build verification** — `RUN test -f /app/main || exit 1` ensures the binary exists before proceeding
- **CVE patching** — Enterprise Dockerfiles include targeted patches (e.g., `apk upgrade --no-cache openssl` for CVE-2026-22796)
- **Minimal runtime dependencies** — The FIPS base image provides only essential libraries (`musl`, `libgcc`, `ca-certificates`)

```dockerfile
# Runtime stage excerpt (production)
FROM <fips-validated-alpine-base>
WORKDIR /app

COPY --from=builder /app/main .
COPY --from=builder /app/docker-entrypoint.sh .

RUN mkdir -p $APP_DIR/logs
USER appuser

ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["/app/main"]
```

<Frame>
<img src="/media/security/hardned-base-image.png" alt="FIPS-compliant hardened base image in Dockerfile" />
</Frame>

### Docker Scout

[Docker Scout](https://docs.docker.com/scout/) is enabled at the Docker Hub repository level for the
`maximhq/bifrost` image. Every image pushed to Docker Hub is automatically scanned for CVEs against
continuously updated vulnerability databases.

<Frame>
<img src="/media/security/scout-image-score.png" alt="Docker Scout image score showing vulnerability assessment" />
</Frame>

### GCP Artifact Registry Scanning

Enterprise images are pushed to **GCP Artifact Registry** (and AWS ECR for select environments).
GCP Artifact Registry provides [built-in vulnerability scanning](https://cloud.google.com/artifact-registry/docs/analysis)
that automatically analyzes container images for OS and language package vulnerabilities.

---

## Supply Chain Security

### GitHub Actions SHA Pinning

All GitHub Actions in the open-source repository are pinned to exact commit SHAs — not mutable version
tags. This prevents supply chain attacks where a compromised action maintainer could push malicious code
to an existing tag.

```yaml
# Every action is pinned to a full SHA with a version comment
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
- uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
```

**Coverage:**

| Repository | Pinning Strategy | Actions Pinned |
| --- | --- | --- |
| `bifrost` (OSS) | Full SHA with version comment | 103 / 103 (100 %) |
| `bifrost-enterprise` | Version tags (`@v4`) | Upgrade planned |

### NPM Provenance

Published npm packages include [SLSA provenance attestations](https://docs.npmjs.com/generating-provenance-statements),
providing a verifiable link between the published package and its source commit.

```yaml
permissions:
id-token: write # Required for npm provenance

# ...
npm publish --provenance --access public
```

### Dependency Pinning in CI

All language runtimes are pinned to specific versions across CI workflows to ensure reproducible builds
and prevent unexpected behavior from runtime updates.

| Runtime | Pinned Version | Used For |
| --- | --- | --- |
| Go | `1.26.1` | Core build, tests |
| Node | `25` | UI build, npm packages |
| Python | `3.11` | Integration and governance tests |
| uv | SHA-pinned via `astral-sh/setup-uv` | Python package management |

Python test dependencies are locked via `uv.lock` files for deterministic, reproducible installs:

```
tests/integrations/uv.lock
tests/governance/uv.lock
```

<Frame>
<img src="/media/security/dep-pinning.png" alt="Pinned dependency versions across CI workflows" />
</Frame>

---

## Dependency Management — Dependabot

Dependabot monitors four ecosystems on a weekly schedule, automatically opening pull requests for
outdated or vulnerable dependencies.

<CardGroup cols={2}>
<Card title="Go Modules" icon="golang">
Covers `/core`, `/framework`, `/transports`, `/plugins/*`, and `/examples/**`
</Card>
<Card title="npm Packages" icon="npm">
Covers `/ui`, `/npx`, and `/examples/**`
</Card>
<Card title="Docker Images" icon="docker">
Monitors base images in `/transports`
</Card>
<Card title="GitHub Actions" icon="github">
Tracks action version updates across all workflows
</Card>
</CardGroup>

A separate **Dependabot Alerts** workflow runs daily and automatically
creates GitHub issues for any open Dependabot security alerts, categorized by severity and ecosystem.

---

## Code Analysis — CodeQL

GitHub's [CodeQL](https://codeql.github.com/) performs semantic code analysis on every push and pull request,
complementing Snyk's SAST coverage with GitHub-native findings.

- Analyzes Go and JavaScript/TypeScript codebases
- Detects security vulnerabilities, bugs, and code quality issues using GitHub's query suites
- Results appear directly in the GitHub Security tab alongside Snyk findings

<Frame>
<img src="/media/security/codeql.png" alt="CodeQL analysis results in GitHub Security tab" />
</Frame>

---

## Workflow & Network Security

### Principle of Least Privilege

All GitHub Actions workflows follow the principle of least privilege. Permissions are set at the
job level, not the workflow level, and are scoped to the minimum required.

| Permission | Granted To | Reason |
| --- | --- | --- |
| `contents: read` | All jobs (default) | Read repository code |
| `contents: write` | Tag creation, releases | Create git tags and releases |
| `security-events: write` | Snyk jobs | Upload SARIF to Security tab |
| `id-token: write` | Cloud auth, npm publish | OIDC federation, npm provenance |
| `pull-requests: write` | PR test reporters | Post test results as PR comments |

### Tailscale VPN

Enterprise deployment workflows authenticate through [Tailscale](https://tailscale.com/) before
accessing any infrastructure. This ensures that CI/CD runners can only reach deployment targets
through an encrypted, identity-aware network — never over the public internet.

```yaml
- name: Authenticate Tailscale
uses: tailscale/github-action@v4
with:
oauth-client-id: '${{ secrets.TS_OAUTH_CLIENT_ID }}'
oauth-secret: '${{ secrets.TS_OAUTH_SECRET }}'
tags: 'tag:gha-ci'
version: 1.84.0
```

- **OAuth-based authentication** — No long-lived API keys; runners authenticate via OAuth client credentials
- **Version pinned** — Tailscale `1.84.0` to prevent unexpected behavior from updates
- **Tagged runners** — `gha-ci` tag enables Tailscale ACL policies scoped to CI/CD access

### StepSecurity

[StepSecurity](https://www.stepsecurity.io/) automatically hardens GitHub Actions workflows by applying
security best practices across all CI/CD pipelines.

- Adds `permissions` blocks to workflows that are missing them
- Pins action versions to full SHAs where mutable tags were used
- Detects insecure patterns like unquoted interpolations and artifact poisoning risks

<Frame>
<img src="/media/security/step-security.png" alt="StepSecurity automated security hardening applied to GitHub Actions workflows" />
</Frame>

### CODEOWNERS

Critical paths in the repository are protected by a `CODEOWNERS` file, ensuring that changes to
security-sensitive areas require review from designated maintainers before merging.

<Frame>
<img src="/media/security/codeowners.png" alt="CODEOWNERS file enforcing review gates on security-critical paths" />
</Frame>
20 changes: 4 additions & 16 deletions transports/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,14 @@ RUN go build \
-o /app/main \
./bifrost-http

# Verify build succeeded
# Verify build succeeded and prepare entrypoint
RUN test -f /app/main || (echo "Build failed" && exit 1)
RUN chmod +x /build/transports/docker-entrypoint.sh
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Wrong path for chmod in builder stage

The builder's WORKDIR is /app and the transports directory is copied there with COPY transports/ ./, so docker-entrypoint.sh lands at /app/docker-entrypoint.sh. The path /build/transports/docker-entrypoint.sh does not exist in this build stage — chmod will fail with a non-zero exit code, causing the Docker build to fail entirely.

This path is correct for Dockerfile.local (where WORKDIR is /build), but it was incorrectly copied verbatim here.

Suggested change
RUN chmod +x /build/transports/docker-entrypoint.sh
RUN chmod +x /app/docker-entrypoint.sh


# --- Runtime Stage: Minimal runtime image ---
FROM alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
FROM bifrosthq/dhi-alpine-base:3.22-fips_bifrost-v27032026
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New base image not pinned to a digest

The original base image was digest-pinned (alpine:3.23.3@sha256:25109...), which the project's own security documentation highlights as a best practice to prevent supply-chain attacks via mutable tags. The new FIPS base image uses only a mutable version tag (bifrosthq/dhi-alpine-base:3.22-fips_bifrost-v27032026) with no @sha256:... digest.

Consider pinning to the image digest for reproducible and tamper-resistant builds:

FROM bifrosthq/dhi-alpine-base:3.22-fips_bifrost-v27032026@sha256:<digest>

The same applies to Dockerfile.local line 72.

WORKDIR /app

# Install runtime dependencies for CGO-enabled binary
# musl: C standard library (required for CGO binaries)
# libgcc: GCC runtime library
# ca-certificates: For HTTPS connections
RUN apk add --no-cache musl libgcc ca-certificates wget

# Create data directory and set up user
COPY --from=builder /app/main .
COPY --from=builder /app/docker-entrypoint.sh .
Expand Down Expand Up @@ -86,21 +81,14 @@ ENV GOGC="" \
GOMEMLIMIT=""


RUN mkdir -p $APP_DIR/logs && \
adduser -D -s /bin/sh appuser && \
chown -R appuser:appuser /app && \
chmod +x /app/docker-entrypoint.sh
RUN mkdir -p $APP_DIR/logs
USER appuser
Comment on lines +84 to 85
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing chown for appuser after removing user-creation block

The previous Dockerfile included chown -R appuser:appuser /app alongside the adduser call. Both were removed here, meaning /app (including /app/data/logs and the copied binaries/scripts) is now owned by root. Since the container switches to appuser immediately after, the application may fail at runtime when attempting to write logs or any other data into /app/data/logs.

The FIPS base image is expected to provide appuser, but without chown, that user will not have write access to directories created as root in this stage. The same issue exists in Dockerfile.local (line 94–95).

Consider adding the missing ownership fix:

RUN mkdir -p $APP_DIR/logs && chown -R appuser:appuser /app
USER appuser



# Declare volume for data persistence
VOLUME ["/app/data"]
EXPOSE $APP_PORT

# Health check for container status monitoring
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 -O /dev/null http://127.0.0.1:${APP_PORT}/health || exit 1

# Use entrypoint script that handles volume permissions and argument processing
ENTRYPOINT ["/app/docker-entrypoint.sh"]
CMD ["/app/main"]
Loading
Loading