Safeguard
Security•

From policy to production: defining guardrails once and enforcing them everywhere

Define a guardrail once on the Policy page, compile it into a signed policy bundle, and enforce it with one line of middleware in Node.js, Python, Java, Go and .NET. A walkthrough of how Guard installs and runs, now in early access.

The Safeguard team
9 min read

Most security rules reach production as a wiki page and a ticket for every service team, and each team implements its own version. Six months later, nobody can say which services enforce which rule. The alternative is to define a guardrail once, compile it into a signed policy bundle, and have every service enforce that same bundle with the same semantics, whatever language it's written in.

This post walks through that path with Safeguard Guard: from the Policy page, to a signed bundle, to one line of middleware in Node.js, Python, Java, Go and .NET. It's a practical guide. Guard is in early access and its SDK packages are rolling out over the coming days, so the install steps show how Guard will install once they're available. The previous post explains what the runtime checks themselves catch.

Why "define once" is harder than it sounds

Central policy has to solve four problems that a wiki page never does:

  • Consistency. The same rule has to mean the same thing in a Node service and a Java service. If each language interprets a pattern differently, you have two policies with one name.
  • Integrity. A service enforcing policy has to know the policy really came from you, unmodified.
  • Availability. A service can't stop working because the policy server is down.
  • Visibility. You need to see which version of the policy each service is actually running.

Each step below exists to solve one of those.

Step 1: write guardrails on the Policy page

Guardrails live on the Policy page in Safeguard, not in a file in each repository. A guardrail is a set of checks. Each check has a type (a SQL injection check, an egress allow list, a model allow list, a token budget, an MCP tool rule and so on), an action and a severity. You can scope a guardrail to environments, such as prod or staging, and to services by name or pattern, such as checkout-*.

Every guardrail starts in Monitor mode. Moving one to Enforce takes an explicit confirmation from a person, and it only takes effect where enforcement is allowed all the way down: for your workspace, for the service, for the guardrail and for the individual check. If any of those says Monitor, the check monitors. Enforcement is never turned on by accident in one place.

Some policy checks have no runtime meaning, such as a compliance rule about how often access is reviewed. Rather than silently dropping them, the bundle lists them as not enforced at runtime, so the Policy page can tell you.

Step 2: compile to a signed policy bundle

Safeguard compiles the guardrails that apply to a service and environment into one policy bundle. The bundle is plain data: the rules, their effective modes, and the detection patterns they use. Patterns are restricted to a regular-expression syntax that every language can run identically, so a pattern can't match in Python and miss in Java.

The bundle is signed with Ed25519. A service verifies the signature over the exact bytes it received before it uses the bundle. If the signature is wrong, it rejects the bundle and keeps the last good one.

That covers integrity. Availability and visibility come from how bundles are delivered:

  • Availability. Every verified bundle is cached on disk and verified again on start. If the policy service is unreachable, the application starts with its last good policy. If there's no policy at all, Guard allows traffic and records that it failed open.
  • Visibility. Each service reports which bundle version it holds and when it last synced, so the Policy page shows what's in force where.

For air-gapped environments, a service can load a signed bundle from a file, with no network connection to Safeguard.

Consistency is the hard one. Every language agent is built against the same shared set of test vectors: the same bundle and the same request must produce the same decision in each language.

Step 3: install from registry.safeguard.sh

Guard's SDK packages are rolling out to Safeguard's private registry, registry.safeguard.sh, over the coming days. The commands in this section show how Guard will install once they're available. They won't resolve before then.

Guard installs only from that registry, with a pull token from your workspace. It isn't published to npm, PyPI, Maven Central, NuGet.org, Docker Hub or the Go module proxy.

That's deliberate, and it's a supply-chain decision. A security agent runs inside every service you protect, so it's an obvious target for dependency confusion: an attacker publishes a public package with the same name and waits for a misconfigured build to pick it up. The registry reserves Safeguard's own names (the @safeguard npm scope, safeguard-guard on PyPI, the sh.safeguard Maven group, Safeguard.* on NuGet and the safeguard.sh/ Go prefix). A request for one of those names never falls through to a public registry. If we don't hold it, the answer is a 404.

There are two credentials, with two jobs. The pull token lets your package manager download Guard from the registry. The secret key (sg_sk_...) lets the running service fetch its policy bundle and send decisions back. Keep both out of source control.

Node.js:

# .npmrc: the @safeguard scope only ever resolves from registry.safeguard.sh
@safeguard:registry=https://registry.safeguard.sh/npm/
//registry.safeguard.sh/npm/:_authToken=${SAFEGUARD_TOKEN}

npm install @safeguard/guard@0.9.0

Python:

# One index only, so safeguard-guard is never looked up on pypi.org
pip install --index-url https://token:${SAFEGUARD_TOKEN}@registry.safeguard.sh/pypi/simple/ safeguard-guard==0.9.0

Java:

<!-- ~/.m2/settings.xml: route sh.safeguard (and everything else) through registry.safeguard.sh -->
<settings>
  <servers><server><id>safeguard</id><username>token</username><password>${env.SAFEGUARD_TOKEN}</password></server></servers>
  <mirrors><mirror><id>safeguard</id><mirrorOf>*</mirrorOf><url>https://registry.safeguard.sh/maven/</url></mirror></mirrors>
</settings>
<!-- pom.xml -->
<dependency><groupId>sh.safeguard</groupId><artifactId>guard-agent</artifactId><version>0.9.0</version></dependency>

.NET:

<!-- nuget.config: Safeguard.* packages only from registry.safeguard.sh (package source mapping) -->
<configuration>
  <packageSources>
    <add key="safeguard" value="https://registry.safeguard.sh/nuget/v3/index.json" />
  </packageSources>
  <packageSourceMapping>
    <packageSource key="safeguard"><package pattern="Safeguard.*" /><package pattern="*" /></packageSource>
  </packageSourceMapping>
  <packageSourceCredentials>
    <safeguard><add key="Username" value="token" /><add key="ClearTextPassword" value="%SAFEGUARD_TOKEN%" /></safeguard>
  </packageSourceCredentials>
</configuration>

dotnet add package Safeguard.Guard --version 0.9.0

Go:

# Safeguard Guard Go SDK (safeguard.sh/guard), only from registry.safeguard.sh
export GOPROXY=https://registry.safeguard.sh/go/,off
# Safeguard products are not in sum.golang.org; every other module is still checked. Never GOPRIVATE for it
# (that would bypass the proxy) and never GOSUMDB=off.
export GONOSUMDB=safeguard.sh/*
# ~/.netrc: machine registry.safeguard.sh login token password {TOKEN}

The Go module's go get line will be added here when its first release with the in-process agents is published. Note the Go settings. Safeguard's modules aren't in the public checksum database, so GONOSUMDB exempts only safeguard.sh/*. Every other module is still checked. Don't use GOPRIVATE for it, which would bypass the registry, and never turn checksum checks off.

Verify what you installed

Every Guard release will be signed with the same Safeguard key as Gold builds and carry a CycloneDX SBOM and SLSA provenance. For the proxy image, verification will look like this:

cosign verify --key https://registry.safeguard.sh/_gold/keys/cosign.pub --insecure-ignore-tlog registry.safeguard.sh/guard/proxy:0.9.0
cosign verify-attestation --key https://registry.safeguard.sh/_gold/keys/cosign.pub --insecure-ignore-tlog --type slsaprovenance1 registry.safeguard.sh/guard/proxy:0.9.0

Package files carry their own signature, SBOM and provenance, checked against the same public key. The first post in this series explains why Safeguard signs with a published key rather than using a public transparency log.

Step 4: add one line of middleware

Installing the middleware starts Guard, turns on the library hooks and gives each hook the request it belongs to. That last part is what lets the SQL check know which strings came from the request.

Configuration is the same everywhere, through environment variables:

export SAFEGUARD_GUARD_KEY=sg_sk_...           # your Safeguard secret key
export SAFEGUARD_GUARD_SERVICE=payments-api
export SAFEGUARD_GUARD_ENVIRONMENT=prod

Then one line per framework.

Node.js with Express:

const { guard } = require('@safeguard/guard');
app.use(guard());

Python with FastAPI, Django or Flask:

# FastAPI / Starlette
from safeguard_guard import SafeguardGuard
app.add_middleware(SafeguardGuard)

# Django settings.py
MIDDLEWARE = ["safeguard_guard.django.GuardMiddleware", *MIDDLEWARE]

# Flask
from safeguard_guard.flask import init_app
init_app(app)

Java with Spring Boot: add the Guard Spring Boot starter, and it registers Guard's filter for you. There's no code to write.

.NET with ASP.NET Core:

builder.Services.AddSafeguardGuard();
var app = builder.Build();
app.UseSafeguardGuard();

Go with net/http:

import "safeguard.sh/guard"

http.ListenAndServe(":8080", guard.Middleware(mux))

When you can't change the code

There are zero-code options for each runtime:

  • Node.js: node --require @safeguard/guard/register app.js
  • Python: safeguard-guard run python app.py
  • Java: start the JVM with -javaagent pointing at the Guard agent jar.
  • .NET: a startup hook, set with DOTNET_STARTUP_HOOKS.

Go has no runtime patching, so Go services use the middleware plus drop-in wrappers for the database driver, commands and HTTP client.

For a service you can't touch at all, Guard can also run as a proxy or sidecar in front of it. It installs from the same registry:

echo "$SAFEGUARD_TOKEN" | docker login registry.safeguard.sh --username token --password-stdin
docker pull registry.safeguard.sh/guard/proxy:0.9.0
echo "$SAFEGUARD_TOKEN" | helm registry login registry.safeguard.sh --username token --password-stdin
helm install guard oci://registry.safeguard.sh/charts/guard --version 0.1.0 --namespace safeguard-guard --create-namespace

A proxy sees requests, responses and outbound traffic, the same as any gateway. It can't see the SQL query a request turns into. That's the reason the in-process middleware is the primary path and the proxy is the alternative.

Step 5: watch in Monitor, then enforce

With the middleware in, nothing is blocked yet. Every guardrail is in Monitor mode, and the decision log fills with what Guard would have done: the check that fired, the hook, the route and where the input came from, without the values.

Run it on real traffic long enough to cover your normal patterns, such as batch jobs, admin tools and the odd partner integration. Tune the checks that fire on legitimate traffic. Then move one guardrail at a time to Enforce, starting with the ones that have stayed quiet.

If something goes wrong, the kill switch turns every hook into a no-op without a code change. Set it and restart the service:

export SAFEGUARD_GUARD_DISABLED=1

Guard fails open by default, so a policy outage or an internal error lets traffic through and leaves a record that it did.

The honest status

Safeguard Guard is in early access, and its SDK packages are rolling out to registry.safeguard.sh over the coming days. It hasn't been tested by customers yet, and we're looking for the first teams to run it in Monitor mode and tell us where it's wrong. The in-process checks are designed for sub-millisecond overhead. We aren't publishing latency figures yet, so measure it on your own traffic before you enforce anything.

Guard is library- and framework-level runtime protection. It doesn't hook the kernel or system calls, and it only sees what passes through the libraries it hooks. Within that boundary, it gives you one policy, written once, enforced the same way in every service.

More on the product: Safeguard Guard. Pricing is available on request. To request early access, book a call.

Never miss an update

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

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.