Skip to content

Commit e84620b

Browse files
committed
Verify application sandbox at startup
1 parent cea6e20 commit e84620b

48 files changed

Lines changed: 1662 additions & 140 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
kind: Security
2+
body: Package a trusted startup verifier into application images and fail closed unless seccomp, no-new-privileges, and empty capability sets are active before workload execution.

‎docs/CONTROLLED_SESSION_DESIGN.md‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ summary: Capability-scoped execution sessions that inherit Reploy's global conta
99
## Status
1010

1111
- Decision state: Focused review complete; high-level decisions approved
12-
- Implementation state: Not started
12+
- Implementation state: Global sandbox prerequisites in progress; session lifecycle not started
1313
- Initial runtime: Linux containers under Docker
1414
- Motivating clients: OmegaFlow recording and sandboxed AI agents
1515

@@ -919,8 +919,15 @@ commands directly as the final identity, drops all capabilities, enables
919919
`no-new-privileges`, explicitly selects Docker's built-in seccomp profile, and
920920
prohibits privileged mode, host namespaces, and host devices in the common
921921
plan. Live Docker tests inspect both runtime paths. Trusted production startup
922-
verification, mount/root authority, network denial, and resource limits remain
923-
separate prerequisite slices.
922+
verification is also implemented: Reploy packages the platform-specific probe
923+
in a final runtime layer, records that layer outside the provider graph, and
924+
uses its fixed verify-and-exec contract as the outermost process for persistent
925+
workloads, private-environment workloads, transient commands, shells, and
926+
lifecycle commands. The verifier fails closed unless `/proc/self/status`
927+
reports seccomp filtering, `no-new-privileges`, and empty effective, permitted,
928+
and bounding capability sets before it executes the exact application argv.
929+
Mount/root authority, network denial, and resource limits remain separate
930+
prerequisite slices.
924931

925932
### Slice 2: Controlled-Session Lifecycle Core
926933

‎internal/deploy/build_lock.go‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type BuildLockV1 struct {
2828
Nodes []NodeLockV1 `json:"nodes"`
2929
Catalog []providers.RealizedOutput `json:"catalog"`
3030
RuntimePolicy RuntimePolicyV1 `json:"runtime_policy"`
31+
RuntimeLayer ApplicationRuntimeLayerV1 `json:"runtime_layer"`
3132
ValidationRecord providerstore.StoreObjectRef `json:"validation_record"`
3233
FinalImage providers.RealizedImageV1 `json:"final_image"`
3334
}
@@ -152,6 +153,14 @@ func ValidateBuildLockV1(lock BuildLockV1, validateProfileOwner providers.Requir
152153
if err := ValidateRuntimePolicyV1(lock.RuntimePolicy); err != nil {
153154
return fmt.Errorf("build lock runtime policy: %w", err)
154155
}
156+
if err := ValidateApplicationRuntimeLayerV1(lock.RuntimeLayer, lock.Platform); err != nil {
157+
return fmt.Errorf("build lock runtime layer: %w", err)
158+
}
159+
if lock.RuntimeLayer.Verifier.Schema != lock.RuntimePolicy.StartupVerifier.Schema ||
160+
lock.RuntimeLayer.Verifier.RecipeVersion != lock.RuntimePolicy.StartupVerifier.RecipeVersion ||
161+
lock.RuntimeLayer.Verifier.Path != lock.RuntimePolicy.StartupVerifier.Path {
162+
return fmt.Errorf("build lock runtime layer verifier does not match the runtime policy")
163+
}
155164
if err := lock.ValidationRecord.Validate(); err != nil {
156165
return fmt.Errorf("build lock validation record: %w", err)
157166
}
@@ -202,10 +211,13 @@ func validateBuildLockImageLineage(lock BuildLockV1) error {
202211
}
203212
current = node.Result
204213
}
205-
if lock.FinalImage.RootFSSubject != current.RootFSSubject {
214+
if lock.RuntimeLayer.Upstream != current {
215+
return fmt.Errorf("build lock runtime layer upstream does not match the final graph prefix")
216+
}
217+
if lock.FinalImage.RootFSSubject != lock.RuntimeLayer.Result.RootFSSubject {
206218
return fmt.Errorf(
207-
"build lock final image root filesystem %s does not match the final graph prefix %s",
208-
lock.FinalImage.RootFSSubject, current.RootFSSubject,
219+
"build lock final image root filesystem %s does not match the application runtime layer %s",
220+
lock.FinalImage.RootFSSubject, lock.RuntimeLayer.Result.RootFSSubject,
209221
)
210222
}
211223
return nil

‎internal/deploy/build_lock_test.go‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,28 @@ func validBuildLock(t *testing.T) BuildLockV1 {
2626
if err != nil {
2727
t.Fatal(err)
2828
}
29+
upstream := providers.RealizedImageV1{Digest: base.ConfigDigest, ConfigDigest: base.ConfigDigest, RootFSSubject: baseRootFS}
30+
verifier := ApplicationStartupVerifierContractV1()
31+
verifier.Artifact = buildLockTestDigest("6")
32+
verifier.Size = "123"
33+
transaction, err := ApplicationRuntimeLayerTransactionDigestV1(verifier, upstream, platform)
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
result := providers.RealizedImageV1{Digest: buildLockTestDigest("5"), ConfigDigest: buildLockTestDigest("5"), RootFSSubject: buildLockTestDigest("7")}
2938
return BuildLockV1{
3039
Schema: BuildLockSchemaV1, BlueprintDigest: buildLockTestDigest("0"), Overlay: EmptyRequestOverlayV1(),
3140
PackageOverrides: EmptyPackageOverrideIntentV1("demo"),
3241
ResolvedRequestDigest: buildLockTestDigest("1"), Platform: platform,
3342
Base: base,
3443
Graph: ProviderGraphLockV1{Nodes: []providers.NodeID{"base"}, Edges: []providers.ProviderEdgeV1{}},
3544
Nodes: []NodeLockV1{}, Catalog: []providers.RealizedOutput{}, RuntimePolicy: validRuntimePolicy(),
45+
RuntimeLayer: ApplicationRuntimeLayerV1{
46+
Schema: ApplicationRuntimeLayerSchemaV1, Verifier: verifier, TransactionDigest: transaction,
47+
Upstream: upstream, Result: result,
48+
},
3649
ValidationRecord: providerstore.StoreObjectRef{Kind: providerstore.ValidationRecordKind, Digest: buildLockTestDigest("4")},
37-
FinalImage: providers.RealizedImageV1{Digest: buildLockTestDigest("5"), ConfigDigest: buildLockTestDigest("5"), RootFSSubject: baseRootFS},
50+
FinalImage: result,
3851
}
3952
}
4053

@@ -84,7 +97,11 @@ func addValidAPTNode(t *testing.T, lock *BuildLockV1) {
8497
GeneratedExecutables: []providers.RealizedGeneratedExecutable{},
8598
Outputs: []providers.RealizedOutput{},
8699
}}
87-
lock.FinalImage.RootFSSubject = result.RootFSSubject
100+
lock.RuntimeLayer.Upstream = result
101+
lock.RuntimeLayer.TransactionDigest, err = ApplicationRuntimeLayerTransactionDigestV1(lock.RuntimeLayer.Verifier, result, lock.Platform)
102+
if err != nil {
103+
t.Fatal(err)
104+
}
88105
}
89106

90107
func TestBuildLockV1CanonicalRoundTripAndIdentity(t *testing.T) {
@@ -160,7 +177,7 @@ func TestBuildLockV1RejectsDisconnectedImageLineage(t *testing.T) {
160177
t.Run("final root filesystem", func(t *testing.T) {
161178
lock := validBuildLock(t)
162179
lock.FinalImage.RootFSSubject = buildLockTestDigest("6")
163-
if _, err := BuildLockDigestV1(lock, acceptBuildLockProfile); err == nil || !strings.Contains(err.Error(), "does not match the final graph prefix") {
180+
if _, err := BuildLockDigestV1(lock, acceptBuildLockProfile); err == nil || !strings.Contains(err.Error(), "does not match the application runtime layer") {
164181
t.Fatalf("lineage error = %v", err)
165182
}
166183
})
@@ -179,6 +196,9 @@ func TestBuildLockV1RejectsInvalidNestedIdentity(t *testing.T) {
179196
{name: "missing node lock", mutate: func(value *BuildLockV1) { value.Graph.Nodes = []providers.NodeID{"apt", "base"} }, want: "missing node"},
180197
{name: "nil catalog", mutate: func(value *BuildLockV1) { value.Catalog = nil }, want: "catalog"},
181198
{name: "runtime policy", mutate: func(value *BuildLockV1) { value.RuntimePolicy.Schema = "bad" }, want: "runtime policy"},
199+
{name: "runtime verifier contract", mutate: func(value *BuildLockV1) { value.RuntimeLayer.Verifier.Path = "/bin/true" }, want: "startup verifier"},
200+
{name: "runtime layer transaction", mutate: func(value *BuildLockV1) { value.RuntimeLayer.TransactionDigest = buildLockTestDigest("f") }, want: "transaction digest"},
201+
{name: "runtime layer upstream", mutate: func(value *BuildLockV1) { value.RuntimeLayer.Upstream.ConfigDigest = buildLockTestDigest("f") }, want: "final graph prefix"},
182202
{name: "validation kind", mutate: func(value *BuildLockV1) { value.ValidationRecord.Kind = providerstore.BlobKind }, want: "validation-record"},
183203
{name: "final image", mutate: func(value *BuildLockV1) { value.FinalImage.RootFSSubject = "bad" }, want: "final image"},
184204
}

‎internal/deploy/runtime_policy.go‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ const (
2626
)
2727

2828
type RuntimePolicyV1 struct {
29-
Schema string `json:"schema"`
30-
ProtectedPaths []ProtectedPathV1 `json:"protected_paths"`
31-
Plans []RuntimePlanV1 `json:"plans"`
29+
Schema string `json:"schema"`
30+
StartupVerifier ApplicationStartupVerifierV1 `json:"startup_verifier"`
31+
ProtectedPaths []ProtectedPathV1 `json:"protected_paths"`
32+
Plans []RuntimePlanV1 `json:"plans"`
3233
}
3334

3435
type ProtectedPathV1 struct {
@@ -63,6 +64,9 @@ func ValidateRuntimePolicyV1(policy RuntimePolicyV1) error {
6364
if policy.ProtectedPaths == nil || policy.Plans == nil {
6465
return fmt.Errorf("runtime policy collections must use arrays")
6566
}
67+
if err := ValidateApplicationStartupVerifierV1(policy.StartupVerifier, false); err != nil {
68+
return fmt.Errorf("runtime policy startup verifier: %w", err)
69+
}
6670
for index, protected := range policy.ProtectedPaths {
6771
if err := validateRuntimeAbsolutePath("protected path", protected.Path); err != nil {
6872
return err

‎internal/deploy/runtime_policy_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99

1010
func validRuntimePolicy() RuntimePolicyV1 {
1111
return RuntimePolicyV1{
12-
Schema: RuntimePolicySchemaV1,
12+
Schema: RuntimePolicySchemaV1, StartupVerifier: ApplicationStartupVerifierContractV1(),
1313
ProtectedPaths: []ProtectedPathV1{
1414
{Path: "/.reploy", Kind: ProtectedPathReployRoot, Owner: "reploy"},
1515
{Path: "/opt/app/bin/tool", Kind: ProtectedPathExecutablePath, Owner: "app.tool"},
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package deploy
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
7+
"github.com/omry/reploy/internal/blueprint"
8+
"github.com/omry/reploy/internal/canonical"
9+
"github.com/omry/reploy/internal/providers"
10+
)
11+
12+
const (
13+
ApplicationStartupVerifierSchemaV1 = "application-startup-verifier-v1"
14+
ApplicationStartupVerifierRecipeV1 = "linux-proc-status-verify-exec-v1"
15+
ApplicationStartupVerifierPathV1 = "/opt/reploy/runtime/reploy-probe"
16+
ApplicationRuntimeLayerSchemaV1 = "application-runtime-layer-v1"
17+
)
18+
19+
type ApplicationStartupVerifierV1 struct {
20+
Schema string `json:"schema"`
21+
RecipeVersion string `json:"recipe_version"`
22+
Path string `json:"path"`
23+
Artifact canonical.Digest `json:"artifact"`
24+
Size string `json:"size"`
25+
}
26+
27+
type ApplicationRuntimeLayerV1 struct {
28+
Schema string `json:"schema"`
29+
Verifier ApplicationStartupVerifierV1 `json:"verifier"`
30+
TransactionDigest canonical.Digest `json:"transaction_digest"`
31+
Upstream providers.RealizedImageV1 `json:"upstream"`
32+
Result providers.RealizedImageV1 `json:"result"`
33+
}
34+
35+
func ApplicationStartupVerifierContractV1() ApplicationStartupVerifierV1 {
36+
return ApplicationStartupVerifierV1{
37+
Schema: ApplicationStartupVerifierSchemaV1, RecipeVersion: ApplicationStartupVerifierRecipeV1,
38+
Path: ApplicationStartupVerifierPathV1,
39+
}
40+
}
41+
42+
func ValidateApplicationStartupVerifierV1(verifier ApplicationStartupVerifierV1, requireArtifact bool) error {
43+
want := ApplicationStartupVerifierContractV1()
44+
if verifier.Schema != want.Schema || verifier.RecipeVersion != want.RecipeVersion || verifier.Path != want.Path {
45+
return fmt.Errorf("application startup verifier does not use the supported fixed contract")
46+
}
47+
if !requireArtifact {
48+
if verifier.Artifact != "" || verifier.Size != "" {
49+
return fmt.Errorf("application startup verifier policy contract must not contain an artifact")
50+
}
51+
return nil
52+
}
53+
if err := verifier.Artifact.Validate(); err != nil {
54+
return fmt.Errorf("application startup verifier artifact: %w", err)
55+
}
56+
size, err := strconv.ParseUint(verifier.Size, 10, 64)
57+
if err != nil || size == 0 || strconv.FormatUint(size, 10) != verifier.Size {
58+
return fmt.Errorf("application startup verifier size must be a canonical positive integer")
59+
}
60+
return nil
61+
}
62+
63+
func ApplicationRuntimeLayerTransactionDigestV1(
64+
verifier ApplicationStartupVerifierV1,
65+
upstream providers.RealizedImageV1,
66+
platform blueprint.Platform,
67+
) (canonical.Digest, error) {
68+
if err := ValidateApplicationStartupVerifierV1(verifier, true); err != nil {
69+
return "", err
70+
}
71+
if err := upstream.Validate(); err != nil {
72+
return "", fmt.Errorf("application runtime layer upstream: %w", err)
73+
}
74+
if err := platform.Validate(); err != nil {
75+
return "", fmt.Errorf("application runtime layer platform: %w", err)
76+
}
77+
return canonical.Sum("application-runtime-layer", ApplicationRuntimeLayerSchemaV1, struct {
78+
Verifier ApplicationStartupVerifierV1 `json:"verifier"`
79+
Upstream providers.RealizedImageV1 `json:"upstream"`
80+
Platform blueprint.Platform `json:"platform"`
81+
}{Verifier: verifier, Upstream: upstream, Platform: platform})
82+
}
83+
84+
func ValidateApplicationRuntimeLayerV1(layer ApplicationRuntimeLayerV1, platform blueprint.Platform) error {
85+
if layer.Schema != ApplicationRuntimeLayerSchemaV1 {
86+
return fmt.Errorf("application runtime layer schema must be %q", ApplicationRuntimeLayerSchemaV1)
87+
}
88+
want, err := ApplicationRuntimeLayerTransactionDigestV1(layer.Verifier, layer.Upstream, platform)
89+
if err != nil {
90+
return err
91+
}
92+
if layer.TransactionDigest != want {
93+
return fmt.Errorf("application runtime layer transaction digest does not match its inputs")
94+
}
95+
if err := layer.Result.Validate(); err != nil {
96+
return fmt.Errorf("application runtime layer result: %w", err)
97+
}
98+
if layer.Result.RootFSSubject == layer.Upstream.RootFSSubject {
99+
return fmt.Errorf("application runtime layer result must add the verifier filesystem layer")
100+
}
101+
return nil
102+
}

0 commit comments

Comments
 (0)