Skip to content

Commit 044c8f0

Browse files
feat: verify scheduler image signatures before run (#109)
* feat: verify scheduler image signatures before run * chore: rebuild manpages
1 parent 1c05ee2 commit 044c8f0

13 files changed

Lines changed: 1088 additions & 32 deletions

File tree

.github/workflows/go.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,6 @@ jobs:
165165

166166
- name: Test
167167
run: go test -tags containers_image_openpgp -timeout 20m -v ./...
168+
169+
- name: E2E signature verification (network)
170+
run: go test -tags 'containers_image_openpgp e2e' -timeout 5m -v ./internal/verify/...

cmd/schedctl/run.go

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"schedctl/internal/output"
1212
"schedctl/internal/podman"
1313
"schedctl/internal/schedulers"
14+
"schedctl/internal/verify"
1415
)
1516

1617
func NewRunCmd() *cli.Command {
@@ -41,12 +42,26 @@ Examples:
4142
Local: true,
4243
Category: categoryProcess,
4344
},
45+
&cli.StringFlag{
46+
Name: "trust-policy",
47+
Usage: "path to a YAML trust policy for image signature verification",
48+
Sources: cli.EnvVars("SCHEDCTL_TRUST_POLICY"),
49+
Local: true,
50+
Category: categoryProcess,
51+
},
52+
&cli.BoolFlag{
53+
Name: "allow-unsigned",
54+
Usage: "skip signature verification (NOT recommended; images run with elevated caps and load eBPF)",
55+
Sources: cli.EnvVars("SCHEDCTL_ALLOW_UNSIGNED"),
56+
Local: true,
57+
Category: categoryProcess,
58+
},
4459
},
4560
Action: runAction,
4661
}
4762
}
4863

49-
func runAction(_ context.Context, cmd *cli.Command) error {
64+
func runAction(ctx context.Context, cmd *cli.Command) error {
5065
args := cmd.Args().Slice()
5166
if len(args) == 0 {
5267
return fmt.Errorf("exactly one scheduler ID required")
@@ -57,6 +72,8 @@ func runAction(_ context.Context, cmd *cli.Command) error {
5772
driver := cmd.String("driver")
5873
attach := cmd.Bool("attach")
5974
version := cmd.String("version")
75+
trustPolicyPath := cmd.String("trust-policy")
76+
allowUnsigned := cmd.Bool("allow-unsigned")
6077

6178
result, err := schedulers.GetScheduler(schedulerID, version)
6279
if err != nil {
@@ -74,27 +91,50 @@ func runAction(_ context.Context, cmd *cli.Command) error {
7491
_, _ = output.Out("With arguments: %v\n", containerArgs)
7592
}
7693

94+
imageRef := result.ImageURI
95+
if allowUnsigned {
96+
_, _ = output.Out("WARNING: --allow-unsigned set; skipping signature verification for %s\n", result.ImageURI)
97+
} else {
98+
policy, err := verify.LoadPolicy(trustPolicyPath)
99+
if err != nil {
100+
return fmt.Errorf("trust policy: %w", err)
101+
}
102+
verified, err := verify.Image(ctx, result.ImageURI, policy)
103+
if err != nil {
104+
return fmt.Errorf(
105+
"image signature verification failed for %s: %w (pass --allow-unsigned to override)",
106+
result.ImageURI, err,
107+
)
108+
}
109+
signer := "unknown"
110+
if len(verified.Signers) > 0 {
111+
signer = verified.Signers[0].Describe()
112+
}
113+
_, _ = output.Out("Verified %s (signer: %s)\n", verified.ImageRef, signer)
114+
imageRef = verified.ImageRef
115+
}
116+
77117
if driver == constants.CONTAINERD {
78118
client, err := containerd.NewClient()
79119
if err != nil {
80120
panic(err)
81121
}
82122
defer client.Close()
83123

84-
err = containerd.Run(client, result.ImageURI, schedulerID, attach, true, containerArgs)
124+
err = containerd.Run(client, imageRef, schedulerID, attach, true, containerArgs)
85125
if err != nil {
86126
return err
87127
}
88128
}
89129

90130
if driver == constants.PODMAN {
91-
err := podman.Run(result.ImageURI, schedulerID, attach, containerArgs)
131+
err := podman.Run(imageRef, schedulerID, attach, containerArgs)
92132
if err != nil {
93133
panic(err)
94134
}
95135

96136
if !attach {
97-
_, _ = output.Out("Container %s started successfully\n", result.ImageURI)
137+
_, _ = output.Out("Container %s started successfully\n", imageRef)
98138
}
99139
}
100140

cmd/schedctl/run_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,30 @@ func TestRunCmdAttachFlagIsCategorized(t *testing.T) {
5151
assert.True(t, ok, "attach flag should be a *cli.BoolFlag")
5252
assert.NotEmpty(t, attachFlag.Category, "attach flag should be assigned to a category for help output")
5353
}
54+
55+
func TestRunCmdHasTrustPolicyFlag(t *testing.T) {
56+
runCmd := cmd.NewRunCmd()
57+
58+
flag := lookupFlag(runCmd.Flags, "trust-policy")
59+
assert.NotNil(t, flag, "run command should have 'trust-policy' flag")
60+
61+
stringFlag, ok := flag.(*cli.StringFlag)
62+
assert.True(t, ok, "trust-policy flag should be a StringFlag")
63+
assert.True(t, stringFlag.Local, "trust-policy flag should be local to the run command")
64+
assert.Contains(t, stringFlag.Sources.EnvKeys(), "SCHEDCTL_TRUST_POLICY",
65+
"trust-policy flag should read SCHEDCTL_TRUST_POLICY env var")
66+
}
67+
68+
func TestRunCmdHasAllowUnsignedFlag(t *testing.T) {
69+
runCmd := cmd.NewRunCmd()
70+
71+
flag := lookupFlag(runCmd.Flags, "allow-unsigned")
72+
assert.NotNil(t, flag, "run command should have 'allow-unsigned' flag")
73+
74+
boolFlag, ok := flag.(*cli.BoolFlag)
75+
assert.True(t, ok, "allow-unsigned flag should be a BoolFlag")
76+
assert.False(t, boolFlag.Value, "allow-unsigned should default to false (verification on by default)")
77+
assert.True(t, boolFlag.Local, "allow-unsigned flag should be local to the run command")
78+
assert.Contains(t, boolFlag.Sources.EnvKeys(), "SCHEDCTL_ALLOW_UNSIGNED",
79+
"allow-unsigned flag should read SCHEDCTL_ALLOW_UNSIGNED env var")
80+
}

dist/man/schedctl-run.1

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ schedctl-run \- Run a specific scheduler
99
schedctl-run
1010

1111
.EX
12+
[--allow-unsigned]
1213
[--attach|-a]
1314
[--driver|-d]=[value]
15+
[--trust-policy]=[value]
1416
[--version]=[value]
1517
.EE
1618

@@ -37,10 +39,16 @@ schedctl run SCHEDULER [-- ARGS...]
3739

3840

3941
.SH GLOBAL OPTIONS
42+
\fB--allow-unsigned\fP: skip signature verification (NOT recommended; images run with elevated caps and load eBPF)
43+
44+
.PP
4045
\fB--attach, -a\fP: attach to the current process instead of detaching
4146

4247
.PP
4348
\fB--driver, -d\fP="": container runtime to use: containerd, podman (default: "podman")
4449

50+
.PP
51+
\fB--trust-policy\fP="": path to a YAML trust policy for image signature verification
52+
4553
.PP
4654
\fB--version\fP="": scheduler version (image tag) to run, e.g. v1.0.0

dist/man/schedctl.1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,15 @@ list available schedulers
3535
.SH run
3636
Run a specific scheduler
3737

38+
.PP
39+
\fB--allow-unsigned\fP: skip signature verification (NOT recommended; images run with elevated caps and load eBPF)
40+
3841
.PP
3942
\fB--attach, -a\fP: attach to the current process instead of detaching
4043

44+
.PP
45+
\fB--trust-policy\fP="": path to a YAML trust policy for image signature verification
46+
4147
.PP
4248
\fB--version\fP="": scheduler version (image tag) to run, e.g. v1.0.0
4349

go.mod

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ require (
77
github.com/containerd/containerd/v2 v2.2.0
88
github.com/containers/podman/v5 v5.8.2
99
github.com/google/go-containerregistry v0.21.5
10+
github.com/sigstore/cosign/v2 v2.6.3
11+
github.com/sigstore/rekor v1.5.1
12+
github.com/sigstore/sigstore v1.10.5
1013
github.com/stretchr/testify v1.11.1
1114
github.com/tmc/scp v0.0.0-20170824174625-f7b48647feef
1215
github.com/urfave/cli-docs/v3 v3.1.0
1316
github.com/urfave/cli/v3 v3.8.0
1417
golang.org/x/crypto v0.50.0
18+
sigs.k8s.io/yaml v1.6.0
1519
)
1620

1721
require (
@@ -54,14 +58,14 @@ require (
5458
github.com/spf13/pflag v1.0.10 // indirect
5559
go.opencensus.io v0.24.0 // indirect
5660
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
57-
go.opentelemetry.io/otel v1.39.0 // indirect
58-
go.opentelemetry.io/otel/metric v1.39.0 // indirect
59-
go.opentelemetry.io/otel/trace v1.39.0 // indirect
61+
go.opentelemetry.io/otel v1.41.0 // indirect
62+
go.opentelemetry.io/otel/metric v1.41.0 // indirect
63+
go.opentelemetry.io/otel/trace v1.41.0 // indirect
6064
golang.org/x/net v0.52.0 // indirect
6165
golang.org/x/sync v0.20.0 // indirect
6266
golang.org/x/sys v0.43.0
6367
golang.org/x/text v0.36.0 // indirect
64-
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
68+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
6569
google.golang.org/grpc v1.79.3 // indirect
6670
google.golang.org/protobuf v1.36.11 // indirect
6771
gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -73,7 +77,10 @@ require (
7377
github.com/BurntSushi/toml v1.5.0 // indirect
7478
github.com/VividCortex/ewma v1.2.0 // indirect
7579
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
80+
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
81+
github.com/blang/semver v3.5.1+incompatible // indirect
7682
github.com/blang/semver/v4 v4.0.0 // indirect
83+
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
7784
github.com/cespare/xxhash/v2 v2.3.0 // indirect
7885
github.com/chzyer/readline v1.5.1 // indirect
7986
github.com/containerd/plugin v1.0.0 // indirect
@@ -82,31 +89,68 @@ require (
8289
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
8390
github.com/containers/ocicrypt v1.2.1 // indirect
8491
github.com/containers/psgo v1.9.1-0.20250826150930-4ae76f200c86 // indirect
92+
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
8593
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
8694
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
8795
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
8896
github.com/cyphar/filepath-securejoin v0.5.2 // indirect
97+
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
98+
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
8999
github.com/disiqueira/gotree/v3 v3.0.2 // indirect
90100
github.com/docker/cli v29.4.0+incompatible // indirect
91101
github.com/docker/distribution v2.8.3+incompatible // indirect
92102
github.com/docker/docker v28.5.2+incompatible // indirect
93103
github.com/docker/docker-credential-helpers v0.9.4 // indirect
94104
github.com/docker/go-connections v0.6.0 // indirect
95105
github.com/docker/go-units v0.5.0 // indirect
106+
github.com/dustin/go-humanize v1.0.1 // indirect
96107
github.com/fsnotify/fsnotify v1.9.0 // indirect
108+
github.com/go-chi/chi/v5 v5.2.5 // indirect
97109
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
110+
github.com/go-openapi/analysis v0.24.3 // indirect
111+
github.com/go-openapi/errors v0.22.7 // indirect
112+
github.com/go-openapi/jsonpointer v0.22.5 // indirect
113+
github.com/go-openapi/jsonreference v0.21.5 // indirect
114+
github.com/go-openapi/loads v0.23.3 // indirect
115+
github.com/go-openapi/runtime v0.29.3 // indirect
116+
github.com/go-openapi/spec v0.22.4 // indirect
117+
github.com/go-openapi/strfmt v0.26.0 // indirect
118+
github.com/go-openapi/swag v0.25.5 // indirect
119+
github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
120+
github.com/go-openapi/swag/conv v0.25.5 // indirect
121+
github.com/go-openapi/swag/fileutils v0.25.5 // indirect
122+
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
123+
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
124+
github.com/go-openapi/swag/loading v0.25.5 // indirect
125+
github.com/go-openapi/swag/mangling v0.25.5 // indirect
126+
github.com/go-openapi/swag/netutils v0.25.5 // indirect
127+
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
128+
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
129+
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
130+
github.com/go-openapi/validate v0.25.2 // indirect
131+
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
98132
github.com/godbus/dbus/v5 v5.1.1-0.20241109141217-c266b19b28e9 // indirect
99133
github.com/golang/protobuf v1.5.4 // indirect
134+
github.com/golang/snappy v0.0.4 // indirect
135+
github.com/google/certificate-transparency-go v1.3.2 // indirect
100136
github.com/google/go-intervals v0.0.2 // indirect
101137
github.com/gorilla/mux v1.8.1 // indirect
102138
github.com/gorilla/schema v1.4.1 // indirect
139+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // indirect
103140
github.com/hashicorp/errwrap v1.1.0 // indirect
141+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
104142
github.com/hashicorp/go-multierror v1.1.1 // indirect
143+
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
144+
github.com/in-toto/attestation v1.1.2 // indirect
145+
github.com/in-toto/in-toto-golang v0.9.0 // indirect
146+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
147+
github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect
105148
github.com/jinzhu/copier v0.4.0 // indirect
106149
github.com/json-iterator/go v1.1.12 // indirect
107150
github.com/kevinburke/ssh_config v1.4.0 // indirect
108151
github.com/klauspost/pgzip v1.2.6 // indirect
109152
github.com/kr/fs v0.1.0 // indirect
153+
github.com/letsencrypt/boulder v0.20260223.0 // indirect
110154
github.com/manifoldco/promptui v0.9.0 // indirect
111155
github.com/mattn/go-runewidth v0.0.16 // indirect
112156
github.com/mattn/go-sqlite3 v1.14.32 // indirect
@@ -120,35 +164,53 @@ require (
120164
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
121165
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
122166
github.com/morikuni/aec v1.0.0 // indirect
167+
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect
123168
github.com/nxadm/tail v1.4.11 // indirect
169+
github.com/oklog/ulid/v2 v2.1.1 // indirect
124170
github.com/opencontainers/cgroups v0.0.5 // indirect
125171
github.com/opencontainers/runc v1.3.4 // indirect
126172
github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 // indirect
173+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
127174
github.com/pkg/sftp v1.13.9 // indirect
128175
github.com/proglottis/gpgme v0.1.5 // indirect
129176
github.com/rivo/uniseg v0.4.7 // indirect
130177
github.com/russross/blackfriday/v2 v2.1.0 // indirect
131-
github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect
178+
github.com/sassoftware/relic v7.2.1+incompatible // indirect
179+
github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect
180+
github.com/shibumi/go-pathspec v1.3.0 // indirect
132181
github.com/sigstore/fulcio v1.8.5 // indirect
133182
github.com/sigstore/protobuf-specs v0.5.0 // indirect
134-
github.com/sigstore/sigstore v1.10.4 // indirect
183+
github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect
184+
github.com/sigstore/sigstore-go v1.1.4 // indirect
185+
github.com/sigstore/timestamp-authority/v2 v2.0.3 // indirect
135186
github.com/skeema/knownhosts v1.3.2 // indirect
136187
github.com/smallstep/pkcs7 v0.1.1 // indirect
188+
github.com/spf13/cobra v1.10.2 // indirect
137189
github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect
138190
github.com/sylabs/sif/v2 v2.22.0 // indirect
191+
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
139192
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
193+
github.com/theupdateframework/go-tuf v0.7.0 // indirect
194+
github.com/theupdateframework/go-tuf/v2 v2.3.0 // indirect
195+
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
196+
github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c // indirect
197+
github.com/transparency-dev/merkle v0.0.2 // indirect
140198
github.com/ulikunitz/xz v0.5.15 // indirect
141199
github.com/vbatts/tar-split v0.12.2 // indirect
142200
github.com/vbauerster/mpb/v8 v8.10.2 // indirect
143201
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
144202
go.podman.io/common v0.67.1 // indirect
145203
go.podman.io/image/v5 v5.39.2 // indirect
146204
go.podman.io/storage v1.62.0 // indirect
205+
go.uber.org/multierr v1.11.0 // indirect
206+
go.uber.org/zap v1.27.1 // indirect
147207
go.yaml.in/yaml/v2 v2.4.3 // indirect
208+
go.yaml.in/yaml/v3 v3.0.4 // indirect
209+
golang.org/x/mod v0.35.0 // indirect
210+
golang.org/x/oauth2 v0.36.0 // indirect
148211
golang.org/x/term v0.42.0 // indirect
149212
golang.org/x/time v0.14.0 // indirect
150-
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b // indirect
213+
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
151214
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
152-
sigs.k8s.io/yaml v1.6.0 // indirect
153215
tags.cncf.io/container-device-interface v1.0.1 // indirect
154216
)

0 commit comments

Comments
 (0)