1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-04-11 11:42:15 +02:00

feat: sign docker images with cosign (#2423)

* feat: sign docker images with cosign

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: improve sign logging

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: do not sign if skip publish is set

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: install cosign

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* docs: fix wrong docs

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>
This commit is contained in:
Carlos Alexandro Becker 2021-08-24 11:22:09 -03:00 committed by GitHub
parent 5bdbffc96f
commit ad57a133fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 496 additions and 29 deletions

View File

@ -46,6 +46,7 @@ jobs:
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- uses: sigstore/cosign-installer@main
-
name: Make Setup
run: |

11
cosign.key Normal file
View File

@ -0,0 +1,11 @@
-----BEGIN ENCRYPTED COSIGN PRIVATE KEY-----
eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6
OCwicCI6MX0sInNhbHQiOiJ4ZGhNcWtrc0ErN2F4VU1YcmlVTXFrZW9vWVhGYkhG
b2hWcERJQnM4bEFzPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
Iiwibm9uY2UiOiJKYkFYVGQrZWh0bTVnZGtYN3JNYkl1SjlsT3A3azF3eCJ9LCJj
aXBoZXJ0ZXh0IjoiNnlRZ29sL2doSFEvTnN3bWszZFN3WmhZdGNkK1lvekFGenpr
emFNa2YxT0dxVGphdVJlRGQyeDZtOXZQek1mL2pHV0NEeHY0VGo3eFVOblI3OUhE
UitKNmU1SUduNTBuRkoxNDlVTUk3aWhHeXVDYWJzckZtZTJhZVhaNTRDeFczNjRi
KzVSU1g0SkMxYUNKMlhneEJjVklRbzN0U1VpeDZuRG9zaUZEMWJVUTB5UDVCZ05K
Qnp3dnRsMHBSNVFjQXNCaEM2aHNjQ1VXOXc9PSJ9
-----END ENCRYPTED COSIGN PRIVATE KEY-----

4
cosign.pub Normal file
View File

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdOGf+JZhlk/D16pf3iR14cEjtGdn
/3N3NDg1Ic7+bm/3ccxvrH8WfkqXftxeSzQ9ICqFdr5DVsufVp7mY5pvdw==
-----END PUBLIC KEY-----

View File

@ -21,7 +21,7 @@ import (
"github.com/goreleaser/goreleaser/pkg/context"
)
// Pipe for artifact signing.
// Pipe that signs common artifacts.
type Pipe struct{}
func (Pipe) String() string {
@ -110,7 +110,9 @@ func sign(ctx *context.Context, cfg config.Sign, artifacts []*artifact.Artifact)
if err != nil {
return err
}
ctx.Artifacts.Add(artifact)
if artifact != nil {
ctx.Artifacts.Add(artifact)
}
}
return nil
}
@ -118,6 +120,7 @@ func sign(ctx *context.Context, cfg config.Sign, artifacts []*artifact.Artifact)
func signone(ctx *context.Context, cfg config.Sign, a *artifact.Artifact) (*artifact.Artifact, error) {
env := ctx.Env.Copy()
env["artifact"] = a.Path
env["artifactID"] = a.ExtraOr("ID", "").(string)
name, err := tmpl.New(ctx).WithEnv(env).Apply(expand(cfg.Signature, env))
if err != nil {
@ -152,7 +155,7 @@ func signone(ctx *context.Context, cfg config.Sign, a *artifact.Artifact) (*arti
stdin = f
}
fields := log.Fields{"cmd": cfg.Cmd}
fields := log.Fields{"cmd": cfg.Cmd, "artifact": a.Name}
// The GoASTScanner flags this as a security risk.
// However, this works as intended. The nosec annotation
@ -171,7 +174,9 @@ func signone(ctx *context.Context, cfg config.Sign, a *artifact.Artifact) (*arti
return nil, fmt.Errorf("sign: %s failed: %w: %s", cfg.Cmd, err, b.String())
}
artifactPathBase, _ := filepath.Split(a.Path)
if cfg.Signature == "" {
return nil, nil
}
env["artifact"] = a.Name
name, err = tmpl.New(ctx).WithEnv(env).Apply(expand(cfg.Signature, env))
@ -179,6 +184,7 @@ func signone(ctx *context.Context, cfg config.Sign, a *artifact.Artifact) (*arti
return nil, fmt.Errorf("sign failed: %s: invalid template: %w", a, err)
}
artifactPathBase, _ := filepath.Split(a.Path)
sigFilename := filepath.Base(env["signature"])
return &artifact.Artifact{
Type: artifact.Signature,

View File

@ -0,0 +1,80 @@
package sign
import (
"fmt"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/ids"
"github.com/goreleaser/goreleaser/internal/pipe"
"github.com/goreleaser/goreleaser/internal/semerrgroup"
"github.com/goreleaser/goreleaser/pkg/context"
)
// Pipe that signs docker images and manifests.
type DockerPipe struct{}
func (DockerPipe) String() string {
return "signing docker images"
}
// Default sets the Pipes defaults.
func (DockerPipe) Default(ctx *context.Context) error {
ids := ids.New("docker_signs")
for i := range ctx.Config.DockerSigns {
cfg := &ctx.Config.DockerSigns[i]
if cfg.Cmd == "" {
cfg.Cmd = "cosign"
}
if len(cfg.Args) == 0 {
cfg.Args = []string{"sign", "-key=cosign.key", "$artifact"}
}
if cfg.Artifacts == "" {
cfg.Artifacts = "none"
}
if cfg.ID == "" {
cfg.ID = "default"
}
ids.Inc(cfg.ID)
}
return ids.Validate()
}
// Run executes the Pipe.
func (DockerPipe) Run(ctx *context.Context) error {
if ctx.SkipSign {
return pipe.ErrSkipSignEnabled
}
if ctx.SkipPublish {
return pipe.ErrSkipSignEnabled
}
g := semerrgroup.New(ctx.Parallelism)
for i := range ctx.Config.DockerSigns {
cfg := ctx.Config.DockerSigns[i]
g.Go(func() error {
var filters []artifact.Filter
switch cfg.Artifacts {
case "images":
filters = append(filters, artifact.ByType(artifact.DockerImage))
case "manifests":
filters = append(filters, artifact.ByType(artifact.DockerManifest))
case "all":
filters = append(filters, artifact.Or(
artifact.ByType(artifact.DockerImage),
artifact.ByType(artifact.DockerManifest),
))
case "none":
return pipe.ErrSkipSignEnabled
default:
return fmt.Errorf("invalid list of artifacts to sign: %s", cfg.Artifacts)
}
if len(cfg.IDs) > 0 {
filters = append(filters, artifact.ByIDs(cfg.IDs...))
}
return sign(ctx, cfg, ctx.Artifacts.Filter(artifact.And(filters...)).List())
})
}
return g.Wait()
}

View File

@ -0,0 +1,189 @@
package sign
import (
"os"
"testing"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"github.com/stretchr/testify/require"
)
func TestDockerSignDescription(t *testing.T) {
require.NotEmpty(t, DockerPipe{}.String())
}
func TestDockerSignDefault(t *testing.T) {
ctx := &context.Context{
Config: config.Project{
DockerSigns: []config.Sign{{}},
},
}
err := DockerPipe{}.Default(ctx)
require.NoError(t, err)
require.Equal(t, ctx.Config.DockerSigns[0].Cmd, "cosign")
require.Equal(t, ctx.Config.DockerSigns[0].Signature, "")
require.Equal(t, ctx.Config.DockerSigns[0].Args, []string{"sign", "-key=cosign.key", "$artifact"})
require.Equal(t, ctx.Config.DockerSigns[0].Artifacts, "none")
}
func TestDockerSignDisabled(t *testing.T) {
ctx := context.New(config.Project{})
ctx.Config.DockerSigns = []config.Sign{
{Artifacts: "none"},
}
err := DockerPipe{}.Run(ctx)
require.EqualError(t, err, "artifact signing is disabled")
}
func TestDockerSignSkipped(t *testing.T) {
ctx := context.New(config.Project{})
ctx.SkipSign = true
err := DockerPipe{}.Run(ctx)
require.EqualError(t, err, "artifact signing is disabled")
}
func TestDockerSignSkipPublish(t *testing.T) {
ctx := context.New(config.Project{})
ctx.SkipPublish = true
err := DockerPipe{}.Run(ctx)
require.EqualError(t, err, "artifact signing is disabled")
}
func TestDockerSignInvalidArtifacts(t *testing.T) {
ctx := context.New(config.Project{})
ctx.Config.DockerSigns = []config.Sign{
{Artifacts: "foo"},
}
err := DockerPipe{}.Run(ctx)
require.EqualError(t, err, "invalid list of artifacts to sign: foo")
}
func TestDockerSignArtifacts(t *testing.T) {
key := "testdata/cosign/cosign.key"
cmd := "sh"
args := []string{"-c", "echo ${artifact} > ${signature} && cosign sign -key=" + key + " -upload=false ${artifact} > ${signature}"}
password := "password"
img1 := "ghcr.io/caarlos0/goreleaser-docker-manifest-actions-example:1.2.1-amd64"
img2 := "ghcr.io/caarlos0/goreleaser-docker-manifest-actions-example:1.2.1-arm64v8"
man1 := "ghcr.io/caarlos0/goreleaser-docker-manifest-actions-example:1.2.1"
for name, cfg := range map[string]struct {
Signs []config.Sign
Expected []string
}{
"no signature file": {
Expected: nil, // no sigs
Signs: []config.Sign{
{
Artifacts: "all",
Stdin: &password,
Cmd: "cosign",
Args: []string{"sign", "-key=" + key, "-upload=false", "${artifact}"},
},
},
},
"sign all": {
Expected: []string{
"testdata/cosign/all_img1.sig",
"testdata/cosign/all_img2.sig",
"testdata/cosign/all_man1.sig",
},
Signs: []config.Sign{
{
Artifacts: "all",
Stdin: &password,
Signature: `testdata/cosign/all_${artifactID}.sig`,
Cmd: cmd,
Args: args,
},
},
},
"sign all filtering id": {
Expected: []string{"testdata/cosign/all_filter_by_id_img2.sig"},
Signs: []config.Sign{
{
Artifacts: "all",
IDs: []string{"img2"},
Stdin: &password,
Signature: "testdata/cosign/all_filter_by_id_${artifactID}.sig",
Cmd: cmd,
Args: args,
},
},
},
"sign images only": {
Expected: []string{
"testdata/cosign/images_img1.sig",
"testdata/cosign/images_img2.sig",
},
Signs: []config.Sign{
{
Artifacts: "images",
Stdin: &password,
Signature: "testdata/cosign/images_${artifactID}.sig",
Cmd: cmd,
Args: args,
},
},
},
"sign manifests only": {
Expected: []string{"testdata/cosign/manifests_man1.sig"},
Signs: []config.Sign{
{
Artifacts: "manifests",
Stdin: &password,
Signature: "testdata/cosign/manifests_${artifactID}.sig",
Cmd: cmd,
Args: args,
},
},
},
} {
t.Run(name, func(t *testing.T) {
ctx := context.New(config.Project{})
ctx.Config.DockerSigns = cfg.Signs
t.Cleanup(func() {
for _, f := range cfg.Expected {
require.NoError(t, os.Remove(f))
}
})
ctx.Artifacts.Add(&artifact.Artifact{
Name: img1,
Path: img1,
Type: artifact.DockerImage,
Extra: map[string]interface{}{
"ID": "img1",
},
})
ctx.Artifacts.Add(&artifact.Artifact{
Name: img2,
Path: img2,
Type: artifact.DockerImage,
Extra: map[string]interface{}{
"ID": "img2",
},
})
ctx.Artifacts.Add(&artifact.Artifact{
Name: man1,
Path: man1,
Type: artifact.DockerManifest,
Extra: map[string]interface{}{
"ID": "man1",
},
})
require.NoError(t, DockerPipe{}.Default(ctx))
require.NoError(t, DockerPipe{}.Run(ctx))
var sigs []string
for _, sig := range ctx.Artifacts.Filter(artifact.ByType(artifact.Signature)).List() {
sigs = append(sigs, sig.Name)
}
require.Equal(t, cfg.Expected, sigs)
})
}
}

View File

@ -0,0 +1 @@
*.sig

View File

@ -0,0 +1,11 @@
-----BEGIN ENCRYPTED COSIGN PRIVATE KEY-----
eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6
OCwicCI6MX0sInNhbHQiOiJ4ZGhNcWtrc0ErN2F4VU1YcmlVTXFrZW9vWVhGYkhG
b2hWcERJQnM4bEFzPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
Iiwibm9uY2UiOiJKYkFYVGQrZWh0bTVnZGtYN3JNYkl1SjlsT3A3azF3eCJ9LCJj
aXBoZXJ0ZXh0IjoiNnlRZ29sL2doSFEvTnN3bWszZFN3WmhZdGNkK1lvekFGenpr
emFNa2YxT0dxVGphdVJlRGQyeDZtOXZQek1mL2pHV0NEeHY0VGo3eFVOblI3OUhE
UitKNmU1SUduNTBuRkoxNDlVTUk3aWhHeXVDYWJzckZtZTJhZVhaNTRDeFczNjRi
KzVSU1g0SkMxYUNKMlhneEJjVklRbzN0U1VpeDZuRG9zaUZEMWJVUTB5UDVCZ05K
Qnp3dnRsMHBSNVFjQXNCaEM2aHNjQ1VXOXc9PSJ9
-----END ENCRYPTED COSIGN PRIVATE KEY-----

View File

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdOGf+JZhlk/D16pf3iR14cEjtGdn
/3N3NDg1Ic7+bm/3ccxvrH8WfkqXftxeSzQ9ICqFdr5DVsufVp7mY5pvdw==
-----END PUBLIC KEY-----

View File

@ -64,5 +64,6 @@ var Pipeline = append(
sign.Pipe{}, // sign artifacts
docker.Pipe{}, // create and push docker images
publish.Pipe{}, // publishes artifacts
sign.DockerPipe{}, // sign docker images and manifests
announce.Pipe{}, // announce releases
)

View File

@ -662,6 +662,7 @@ type Project struct {
Changelog Changelog `yaml:",omitempty"`
Dist string `yaml:",omitempty"`
Signs []Sign `yaml:",omitempty"`
DockerSigns []Sign `yaml:"docker_signs,omitempty"`
EnvFiles EnvFiles `yaml:"env_files,omitempty"`
Before Before `yaml:",omitempty"`
Source Source `yaml:",omitempty"`

View File

@ -49,6 +49,7 @@ var Defaulters = []Defaulter{
snapcraft.Pipe{},
checksums.Pipe{},
sign.Pipe{},
sign.DockerPipe{},
docker.Pipe{},
docker.ManifestPipe{},
artifactory.Pipe{},

View File

@ -0,0 +1,89 @@
---
title: Docker Image Signing
---
Signing Docker Images and Manifests is also possible with GoReleaser.
This pipe was designed based on the common [sign](/customization/sign/) pipe having [cosign](https://github.com/sigstore/cosign) in mind.
!!! info
Note that this pipe will run only at the end of the GoReleaser execution, as cosign will change the image in the registry.
To customize the signing pipeline you can use the following options:
```yaml
# .goreleaser.yml
docker_signs:
-
# ID of the sign config, must be unique.
# Only relevant if you want to produce some sort of signature file.
#
# Defaults to "default".
id: foo
# Name/template of the signature file.
#
# Available environment variables:
# - '${artifact}': the path to the artifact that will be signed
# - '${artifactID}': the ID of the artifact that will be signed
#
# Note that with cosign you don't need to use this.
#
# Defaults to empty.
signature: "${artifact}_sig"
# Path to the signature command
#
# Defaults to `cosign`
cmd: cosign
# Command line templateable arguments for the command
#
# defaults to `["sign", "-key=cosign.key", "${artifact}"]`
args: ["sign", "-key=cosign.key", "-upload=false", "${artifact}"]
# Which artifacts to sign
#
# all: all artifacts
# none: no signing
# images: only docker images
# manifests: only docker manifests
#
# defaults to `none`
artifacts: all
# IDs of the artifacts to sign.
#
# Defaults to empty (which implies no ID filtering).
ids:
- foo
- bar
# Stdin data template to be given to the signature command as stdin.
# Defaults to empty
stdin: '{{ .Env.GPG_PASSWORD }}'
# StdinFile file to be given to the signature command as stdin.
# Defaults to empty
stdin_file: ./.password
```
## Common usage example
Assuming you have a `cosign.key` in the repository root and a `COSIGN_PWD`
environment variable, the simplest configuration to sign both Docker images
and manifests would look like this:
```yaml
# .goreleaser.yml
docker_signs:
- artifacts: all
stdin: '{{ .Env.COSIGN_PWD }}'
```
Later on you (and anyone else) can verify the image with:
```sh
cosign verify -key cosign.pub your/image
```

View File

@ -8,7 +8,7 @@ signing key.
GoReleaser provides means to sign both executables and archives.
## Archives
## Usage
Signing works in combination with checksum files and it is generally sufficient
to sign the checksum files only.
@ -30,30 +30,34 @@ To customize the signing pipeline you can use the following options:
signs:
-
# ID of the sign config, must be unique.
#
# Defaults to "default".
id: foo
# name/template of the signature file.
# '${artifact}' is the path to the artifact that should be signed.
# Name/template of the signature file.
#
# defaults to `${artifact}.sig`
# Available environment variables:
# - '${artifact}': the path to the artifact that will be signed
# - '${artifactID}': the ID of the artifact that will be signed
#
# Defaults to `${artifact}.sig`.
signature: "${artifact}_sig"
# path to the signature command
# Path to the signature command
#
# defaults to `gpg`
# Defaults to `gpg`
cmd: gpg2
# command line templateable arguments for the command
# Command line templateable arguments for the command
#
# to sign with a specific key use
# args: ["-u", "<key id, fingerprint, email, ...>", "--output", "${signature}", "--detach-sign", "${artifact}"]
#
# defaults to `["--output", "${signature}", "--detach-sign", "${artifact}"]`
# Defaults to `["--output", "${signature}", "--detach-sign", "${artifact}"]`
args: ["--output", "${signature}", "${artifact}", "{{ .ProjectName }}"]
# which artifacts to sign
# Which artifacts to sign
#
# all: all artifacts
# none: no signing
@ -63,52 +67,61 @@ signs:
# archive: archives from archive pipe
# binary: binaries if archiving format is set to binary
#
# defaults to `none`
# Defaults to `none`
artifacts: all
# IDs of the artifacts to sign.
# Defaults to all.
#
# If `artifacts` is checksum or source, this fields has no effect.
#
# Defaults to empty (which implies no filtering).
ids:
- foo
- bar
# Stdin data template to be given to the signature command as stdin.
#
# Defaults to empty
stdin: '{{ .Env.GPG_PASSWORD }}'
# StdinFile file to be given to the signature command as stdin.
#
# Defaults to empty
stdin_file: ./.password
```
### Limitations
## Signing with cosign
You can sign with any command that outputs a file.
If what you want to use does not do it, you can always hack by setting the
command to `sh -c`. For example:
You can sign you artifacts with [cosign][] as well.
Assuming you have a `cosign.key` in the repository root and a `COSIGN_PWD` environment variable set, a simple usage example would look like this:
```yaml
# .goreleaser.yml
signs:
- cmd: sh
args:
- '-c'
- 'echo "${artifact} is signed and I can prove it" | tee ${signature}'
- cmd: cosign
stdin: '{{ .Env.COSIGN_PWD }}'
args: ["sign-blob", "-key=cosign.key", "-output=${signature}", "${artifact}"]
artifacts: all
```
And it will work just fine. Just make sure to always use the `${signature}`
template variable as the result file name and `${artifact}` as the origin file.
Your users can then verify the signature with:
```sh
cosign verify-blob -key cosign.pub -signature file.tar.gz.sig file.tar.gz
```
## Executables
## Signing executables
Executables can be signed after build using post hooks.
For example you can use [gon][] to create notarized MacOS apps:
### With gon
For example, you can use [gon][] to create notarized MacOS apps:
```yaml
# .goreleaser.yml
builds:
- binary: foo
id: foo
@ -129,8 +142,11 @@ builds:
post: gon gon.hcl
```
**`gon.hcl`:**
```hcl
and:
```terraform
# gon.hcl
#
# The path follows a pattern
# ./dist/BUILD-ID_TARGET/BINARY-NAME
source = ["./dist/foo-macos_darwin_amd64/foo"]
@ -156,4 +172,55 @@ as `extra_files` in the `release` section to make sure they also get uploaded.
You can also check [this issue](https://github.com/goreleaser/goreleaser/issues/1227) for more details.
### With cosign
You can also use [cosign][] to sign the binaries directly,
but you'll need to manually add the `.sig` files to the release and/or archive:
```yaml
# .goreleaser.yml
builds:
- hooks:
post:
- sh -c "echo $COSIGN_PWD | cosign sign-blob -key cosign.key {{ .Path }} > dist/{{ .ProjectName }}_{{ .Version }}_{{ .Target }}.sig"
# add to the release directly:
release:
extra_files:
- glob: dist/*.sig
# or just to the archives:
archives:
- files:
- dist/*.sig
```
While this works, I would recommend using the signing pipe directly.
## Signing Docker images and manifests
Please refer to [Docker Images Signing](/customization/docker_sign/).
## Limitations
You can sign with any command that either outputs a file or modify the file being signed.
If you want to sign with something that writes to `STDOUT` instead of a file,
you can wrap the command inside a `sh -c` execution, for instance:
```yaml
# .goreleaser.yml
signs:
- cmd: sh
args:
- '-c'
- 'echo "${artifact} is signed and I can prove it" | tee ${signature}'
artifacts: all
```
And it will work just fine. Just make sure to always use the `${signature}`
template variable as the result file name and `${artifact}` as the origin file.
[gon]: https://github.com/mitchellh/gon
[cosign]: https://github.com/sigstore/cosign

View File

@ -78,6 +78,7 @@ nav:
- customization/dist.md
- customization/docker.md
- customization/docker_manifest.md
- customization/docker_sign.md
- customization/env.md
- customization/fury.md
- customization/gomod.md