diff --git a/image/signature/policy_eval.go b/image/signature/policy_eval.go index 2d0db05ae4..1ef882aef0 100644 --- a/image/signature/policy_eval.go +++ b/image/signature/policy_eval.go @@ -12,6 +12,7 @@ import ( "github.com/sirupsen/logrus" "go.podman.io/image/v5/internal/private" "go.podman.io/image/v5/internal/unparsedimage" + "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" ) @@ -65,6 +66,9 @@ type PolicyRequirement interface { // WARNING: This validates signatures and the manifest, but does not download or validate the // layers. Users must validate that the layers match their expected digests. isRunningImageAllowed(ctx context.Context, image private.UnparsedImage) (bool, error) + + // isInsecure returns true if the requirement allows images without any signatures. + isInsecure() bool } // PolicyReferenceMatch specifies a set of image identities accepted in PolicyRequirement. @@ -79,8 +83,9 @@ type PolicyReferenceMatch interface { // PolicyContext encapsulates a policy and possible cached state // for speeding up its evaluation. type PolicyContext struct { - Policy *Policy - state policyContextState // Internal consistency checking + Policy *Policy + state policyContextState // Internal consistency checking + rejectInsecure bool } // policyContextState is used internally to verify the users are not misusing a PolicyContext. @@ -132,6 +137,13 @@ func policyIdentityLogName(ref types.ImageReference) string { return ref.Transport().Name() + ":" + ref.PolicyConfigurationIdentity() } +// SetRejectInsecure modifies insecure policy requirement handling. If +// passed `true`, policy checking by IsRunningImageAllowed will ignore the +// "insecureAcceptAnything" policy type. +func (pc *PolicyContext) SetRejectInsecure(val bool) { + pc.rejectInsecure = val +} + // requirementsForImageRef selects the appropriate requirements for ref. func (pc *PolicyContext) requirementsForImageRef(ref types.ImageReference) PolicyRequirements { // Do we have a PolicyTransportScopes for this transport? @@ -278,6 +290,7 @@ func (pc *PolicyContext) IsRunningImageAllowed(ctx context.Context, publicImage return false, PolicyRequirementError("List of verification policy requirements must not be empty") } + wasSecure := false for reqNumber, req := range reqs { // FIXME: supply state allowed, err := req.isRunningImageAllowed(ctx, image) @@ -286,7 +299,15 @@ func (pc *PolicyContext) IsRunningImageAllowed(ctx context.Context, publicImage return false, err } logrus.Debugf(" Requirement %d: allowed", reqNumber) + if !req.isInsecure() { + wasSecure = true + } } + + if pc.rejectInsecure && !wasSecure { + return false, PolicyRequirementError(fmt.Sprintf("No secure policy found for image %s.", transports.ImageName(image.Reference()))) + } + // We have tested that len(reqs) != 0, so at least one req must have explicitly allowed this image. logrus.Debugf("Overall: allowed") return true, nil diff --git a/image/signature/policy_eval_baselayer.go b/image/signature/policy_eval_baselayer.go index f310342d10..5305acf6a5 100644 --- a/image/signature/policy_eval_baselayer.go +++ b/image/signature/policy_eval_baselayer.go @@ -18,3 +18,7 @@ func (pr *prSignedBaseLayer) isRunningImageAllowed(ctx context.Context, image pr logrus.Errorf("signedBaseLayer not implemented yet!") return false, PolicyRequirementError("signedBaseLayer not implemented yet!") } + +func (pr *prSignedBaseLayer) isInsecure() bool { + return false +} diff --git a/image/signature/policy_eval_signedby.go b/image/signature/policy_eval_signedby.go index 21ed59494d..474a781d3b 100644 --- a/image/signature/policy_eval_signedby.go +++ b/image/signature/policy_eval_signedby.go @@ -114,3 +114,7 @@ func (pr *prSignedBy) isRunningImageAllowed(ctx context.Context, image private.U } return false, summary } + +func (pr *prSignedBy) isInsecure() bool { + return false +} diff --git a/image/signature/policy_eval_sigstore.go b/image/signature/policy_eval_sigstore.go index cee04dc4ed..04d9ce8bf2 100644 --- a/image/signature/policy_eval_sigstore.go +++ b/image/signature/policy_eval_sigstore.go @@ -433,3 +433,7 @@ func (pr *prSigstoreSigned) isRunningImageAllowed(ctx context.Context, image pri } return false, summary } + +func (pr *prSigstoreSigned) isInsecure() bool { + return false +} diff --git a/image/signature/policy_eval_simple.go b/image/signature/policy_eval_simple.go index 4ef35e3ad5..37092adaf0 100644 --- a/image/signature/policy_eval_simple.go +++ b/image/signature/policy_eval_simple.go @@ -20,6 +20,10 @@ func (pr *prInsecureAcceptAnything) isRunningImageAllowed(ctx context.Context, i return true, nil } +func (pr *prInsecureAcceptAnything) isInsecure() bool { + return true +} + func (pr *prReject) isSignatureAuthorAccepted(ctx context.Context, image private.UnparsedImage, sig []byte) (signatureAcceptanceResult, *Signature, error) { return sarRejected, nil, PolicyRequirementError(fmt.Sprintf("Any signatures for image %s are rejected by policy.", transports.ImageName(image.Reference()))) } @@ -27,3 +31,7 @@ func (pr *prReject) isSignatureAuthorAccepted(ctx context.Context, image private func (pr *prReject) isRunningImageAllowed(ctx context.Context, image private.UnparsedImage) (bool, error) { return false, PolicyRequirementError(fmt.Sprintf("Running image %s is rejected by policy.", transports.ImageName(image.Reference()))) } + +func (pr *prReject) isInsecure() bool { + return false +} diff --git a/image/signature/policy_eval_test.go b/image/signature/policy_eval_test.go index a140d241d2..07be3c5e68 100644 --- a/image/signature/policy_eval_test.go +++ b/image/signature/policy_eval_test.go @@ -497,3 +497,79 @@ func assertRunningRejectedPolicyRequirement(t *testing.T, allowed bool, err erro assertRunningRejected(t, allowed, err) assert.IsType(t, PolicyRequirementError(""), err) } + +func TestPolicyContextSetRejectInsecure(t *testing.T) { + pc, err := NewPolicyContext(&Policy{Default: PolicyRequirements{NewPRReject()}}) + require.NoError(t, err) + defer func() { + err := pc.Destroy() + require.NoError(t, err) + }() + + // Test default value is false + assert.False(t, pc.rejectInsecure) + + // Test setting to true + pc.SetRejectInsecure(true) + assert.True(t, pc.rejectInsecure) + + // Test setting back to false + pc.SetRejectInsecure(false) + assert.False(t, pc.rejectInsecure) +} + +func TestPolicyContextIsRunningImageAllowedWithRejectInsecure(t *testing.T) { + pc, err := NewPolicyContext(&Policy{ + Default: PolicyRequirements{NewPRReject()}, + Transports: map[string]PolicyTransportScopes{ + "docker": { + "docker.io/testing/manifest:insecureOnly": { + NewPRInsecureAcceptAnything(), + }, + "docker.io/testing/manifest:insecureWithOther": { + NewPRInsecureAcceptAnything(), + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()), + }, + "docker.io/testing/manifest:signedOnly": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()), + }, + }, + }, + }) + require.NoError(t, err) + defer func() { + err := pc.Destroy() + require.NoError(t, err) + }() + + // Test with rejectInsecure=false (default behavior) + // insecureAcceptAnything should be accepted + img := pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:insecureOnly") + res, err := pc.IsRunningImageAllowed(context.Background(), img) + assertRunningAllowed(t, res, err) + + // Test with rejectInsecure=true + pc.SetRejectInsecure(true) + + // insecureAcceptAnything only: should be rejected (leaves no secure requirements) + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:insecureOnly") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assert.Equal(t, false, res) + assert.Error(t, err) + + // insecureAcceptAnything + signed requirement: first requirement has no effect, second is secure and valid + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:insecureWithOther") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assertRunningAllowed(t, res, err) + + // signed requirement only: should work normally + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:signedOnly") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assertRunningAllowed(t, res, err) + + // Test with unsigned image and insecureAcceptAnything + signed requirement: first requirement has no effect, second is secure but rejects + img = pcImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:insecureWithOther") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assert.Equal(t, false, res) + assert.Error(t, err) +}