
Learn how Endura derives least-privilege policy from real build behavior, validates it without disruption, and moves pipelines safely from observation into kernel-level enforcement.
September 10, 2026
6 Min Reads
Endura Security

Learn how Endura derives least-privilege policy from real build behavior, validates it without disruption, and moves pipelines safely from observation into kernel-level enforcement.
In the previous article, we connected a GitHub Actions pipeline to Endura, deployed Runtime Sensor, and captured a baseline from a real build. That gave us visibility into what the build actually does.
Now comes the more important question:
Which of those behaviors should the build be allowed to do?
A build may legitimately compile code, execute scripts, read dependencies, write artifacts, and connect to package repositories. But observed behavior is not automatically trusted behavior. A dependency could perform an unexpected network request. A setup script might access a file once and never need it again. A build path might vary between runners.
Endura's policy workflow is designed around that reality. You start with observed behavior, refine it into an intentional security boundary, validate that boundary without disrupting builds, and only then enable enforcement.
The progression is simple:
Each stage answers a different question.
An Endura policy in derive mode can be as simple as:
{
"mode": "derive"
}
Run representative builds with Runtime Sensor active, then stop the Endura job when the work is complete. Runtime Sensor records the security-relevant behavior it observed and produces the rules needed to represent that activity. Depending on the build, those rules may describe file access, program execution, network connections, process operations, container activity, memory behavior, kernel operations, and other protected actions. This gives you a starting point based on evidence instead of assumptions. But it is only a starting point.
This is the most important idea in the policy lifecycle.
Derivation answers: What happened?
It does not answer: What should be allowed?
Endura cannot know whether every action observed during derivation was intentional.
Suppose a build runs npm install and one dependency unexpectedly attempts to connect to an external host. If that happens during derivation, the connection may appear in the derived policy because Runtime Sensor observed it.
That does not mean you should keep it.
The opposite problem also exists. If a legitimate branch did not execute during derivation, the derived policy cannot contain behavior it never saw. For example, perhaps your normal build ran successfully, but the error-handling path never executed. Or your release workflow was not triggered. Or an optional integration was not used. A good derivation process therefore includes more than one happy-path build. Run representative workloads, including optional paths and failure handling where practical.
The goal is not to blindly accept generated policy. The goal is to start policy review with a record of reality.
Before assigning a derived policy to a pipeline, read it.
You are looking for three kinds of rules:
These are operations the build clearly needs.
Examples might include:
These rules are candidates to remain in the policy.
These deserve investigation.
Examples might include:
Do not simply add permissions because the build performed an operation.
Ask why it performed it.
Some behavior is legitimate but too specific to one execution. Temporary filenames, runner paths, dynamically generated directories, individual service IP addresses, and other values may change from build to build. These usually need refinement rather than deletion.
A useful security policy needs to survive normal variation without becoming unnecessarily broad. Endura provides several policy patterns to help do that.
CI/CD workspaces often move between runners or change from one execution to another. Instead of hardcoding a path such as: /home/runner/work/my-project/my-project/build/output
Endura policies can use the %workspace% variable: %workspace%/build/output
%workspace% expands to the current Endura job's workspace, which makes the policy portable across builds without granting access to unrelated paths.
Other policy variables can represent values such as the user's home directory, host name, user ID, username, and temporary directory when available. The goal is not to make the rule vague. It is to describe the legitimate resource in a way that remains stable.
Build tools often create filenames that include timestamps, hashes, random identifiers, or temporary values.
Imagine derivation produces several similar paths:
/opt/app/tmp/script-1842
/opt/app/tmp/script-7309
/opt/app/tmp/script-9124
You probably do not want a growing policy containing every temporary filename ever generated.
If /opt/app/tmp is the legitimate boundary, the policy can instead allow the stable parent directory.
Endura supports directory-prefix matching for file path arguments, so a rule targeting a directory can cover paths beneath it. This is an important distinction. You are broadening the policy enough to accommodate expected variation, but only within a boundary you have reviewed.
Policies also support brace expansion for related values.
For example, instead of maintaining separate rules for:
/usr/bin/curl|api.example.com|80
/usr/bin/curl|api.example.com|443
/usr/bin/curl|api.example.com|8080
you can express the same intent more cleanly as:
/usr/bin/curl|api.example.com|{80,443,8080}
The same approach can help consolidate executable paths, scripts, ports, and other repeated values. Cleaner policy is easier to understand. And policy that is easier to understand is easier to review safely.
Network behavior deserves particular attention in CI/CD. Build environments frequently hold valuable credentials. A compromised dependency does not need to persist on the runner if it can read a credential and send it somewhere else during the build. Endura can control outbound connections based on the executable, destination, and port.
A rule might permit an expected tool to reach a specific service:
/usr/bin/curl|api.example.com|443
or allow a verified network range: all|140.82.112.0/20|443
Endura can use hostnames in connection rules, although enforcement ultimately operates at the IP layer. CIDR ranges may provide more reliable behavior for services whose addresses change frequently. Derived policies commonly capture specific addresses. Resist the temptation to immediately replace several addresses with a very broad CIDR. First verify that the entire range belongs to the service you intend to trust.
The question should always be: What destination does this build actually need?
Not: What network can I allow so this violation goes away?
all as a Decision
Endura policy rules support the value: all
It means exactly what it sounds like. It matches anything in that position. There are cases where broad access may be legitimate. But every use of all should be intentional.
For example:
{
"ip": {
"connect": ["all"]
}
}
allows every covered outbound connection. That might make a policy easy to deploy, but it also removes the network boundary you were trying to establish. The same principle applies to file access, execution, containers, and other operations. Start with the narrowest rule that represents the legitimate behavior you understand. Broaden it when the workload requires it, not simply because broad rules are easier.
Derivation is only as complete as the builds you ran. Before moving forward, think about legitimate paths that may not have been exercised.
Does the pipeline:
If the answer is yes, either exercise those paths during derivation or deliberately add the permissions they require. This is one reason moving directly from derive to enforce is risky. You want policy to represent expected behavior across the pipeline, not one successful execution.
Once you have reviewed and refined the baseline, change the policy mode to:
{
"mode": "observe"
}
Observe mode evaluates the policy but does not block covered operations.
If the build performs something outside the policy, Endura records a violation and allows the operation to continue. This is where you find out whether the policy you designed matches the behavior of real builds. Run normal workloads. Run pull requests. Run tests. Run builds with and without caches. Run release paths if they behave differently. Let developers work normally. Then review what Endura reports.
When a violation appears in observe mode, do not immediately treat it as either an attack or a false positive.
Treat it as a question: Why did the build need to do this?
There are usually three possibilities.
Perhaps a valid workflow branch was missing from derivation. Update the policy with the narrowest rule that permits the behavior.
Maybe the build wrote to a new temporary filename or reached another approved service address. Refine the existing rule so it represents the stable boundary.
This is the most interesting outcome. The application may not need that network connection. The package manager may not need access to that credential path. The build tool may not need to execute that binary. In that case, do not change the policy. Change the build, dependency, configuration, or workflow instead. This is how observe mode turns policy development into a useful security review rather than a simple allowlist exercise.
Team Server centralizes security policy management for connected pipelines. To create a policy, open Policies, provide a name, description, and version, then add the policy definition. Once the policy is ready, assign it to the pipeline or pipelines it should govern. Policy owners and administrators can manage policy changes, assignments, versions, and related actions from Team Server.
For the first pipeline, keep the scope simple: one pipeline, one reviewed policy. A representative set of observe-mode builds; the objective is confidence, not rollout speed.
There is no magic number of observe-mode builds that makes a policy safe. What matters is coverage.
You should have enough representative executions to be comfortable that:
The Endura documentation recommends moving to enforcement after representative observe-mode runs complete without unexplained violations. That last word matters.
Unexplained.
You do not need a policy that never produces information. You need a policy where you understand why the allowed behavior is allowed.
When you are satisfied with the policy, change:
{
"mode": "observe"
}
to:
{
"mode": "enforce"
}
The meaning of the policy has now changed. In observe mode, an operation outside policy creates a violation. In enforce mode, a covered operation without a matching rule is denied. Endura policies are deny-by-default, so omitted covered operations are not implicitly permitted. Runtime Sensor evaluates security-relevant Linux operations at the kernel boundary using eBPF. In enforce mode, unauthorized covered behavior is blocked before it completes. A policy violation can therefore stop the offending operation and cause the build to fail rather than simply producing an alert after the activity has already occurred. That is the point where the behavioral baseline becomes an actual security boundary.
Consider a pipeline whose policy permits:
Now imagine compromised code attempts to:
If the relevant operation is covered by Endura and no policy rule allows it, enforcement denies it. The build does not need a known malware signature. The package does not need to appear on a blocklist. Someone does not need to identify the dependency as malicious first. The behavior is outside the boundary. That is enough.
Enforcement is not the end of policy management: build systems change, dependencies change, toolchains change, cloud providers change infrastructure, developers add workflows, etc.
When legitimate behavior changes, an enforced policy may begin producing violations. Continue reviewing them. The difference is that once you have reached enforcement, unexpected behavior does not automatically get the benefit of the doubt. Endura blocks the covered operation until policy permits it. That creates a useful forcing function. Instead of silently expanding what a pipeline can do over time, changes to the runtime boundary become visible and reviewable.
A single successful build rarely exercises every legitimate branch. Use derivation to start the policy, not to finish it.
If behavior looks strange, investigate it. Derivation records what happened, including behavior you may not want to permit.
A policy that allows everything will certainly produce fewer violations. It will also provide very little protection. Use broad rules only when they reflect an intentional security decision.
Several observed IP addresses do not automatically justify access to an entire subnet. Verify ownership and required service ranges before broadening network rules.
A policy based only on normal development builds may break when the pipeline encounters an error or performs a release. Include meaningful alternate paths in validation.
A violation means runtime behavior fell outside the boundary you defined. Sometimes the correct response is to change the policy. Sometimes the correct response is to change the build. That distinction is where much of the security value comes from.
There is no universal Endura policy.
A Node.js application, Rust project, container build, kernel package, and infrastructure deployment all behave differently. That is exactly why behavior-based derivation is useful. A good policy is not the shortest policy possible. It is not the policy with the most rules either. A good policy is one your team can explain.
You should be able to look at the important permissions and understand:
If nobody can explain a permission, that permission deserves another look.
The progression from derive to enforce is designed to make least privilege practical in environments that are often too complex to model by hand. That process gives you something more useful than a static description of your pipeline. It gives you an operating boundary based on how the pipeline actually behaves. And once that boundary is enforced at the kernel, software running inside the build no longer gets unlimited freedom simply because it made it into the pipeline.
It gets exactly the access the build requires, and nothing more.



