Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow alter trusted clone plugins and filter them via tag #4074

Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a8fa090
Extend utils.MatchImage by add utils.MatchImageDynamic and utils.Matc…
6543 Aug 25, 2024
9a59c5b
allow-list trusted- and escalated-plugins by exact match
6543 Aug 26, 2024
553b095
document one change
6543 Aug 26, 2024
95f8b6d
document MatchImageDynamic usage for secret plugin filer
6543 Aug 26, 2024
fa83a29
update privilged plugins to allow list
6543 Aug 26, 2024
5dfed30
lint against clone steps with untrusted images
6543 Aug 26, 2024
1d490b7
could be more
6543 Aug 26, 2024
bcea359
Merge branch 'main' into plugin-secret-exact-filter-if-tag-set
6543 Aug 26, 2024
6f60814
Merge branch 'main' into plugin-secret-exact-filter-if-tag-set
6543 Aug 29, 2024
689f7f8
Merge branch 'main' into plugin-secret-exact-filter-if-tag-set
6543 Aug 31, 2024
0e08318
Merge branch 'main' into plugin-secret-exact-filter-if-tag-set
6543 Aug 31, 2024
82d2103
Merge branch 'main' into plugin-secret-exact-filter-if-tag-set
6543 Aug 31, 2024
b95281b
Apply suggestions from code review
6543 Aug 31, 2024
9336afb
clean
6543 Aug 31, 2024
f81a86e
clean
6543 Aug 31, 2024
9fc5be8
Revert "clean"
6543 Aug 31, 2024
c29a416
clean fix
6543 Aug 31, 2024
1bf9001
Merge branch 'main' into plugin-secret-exact-filter-if-tag-set
6543 Sep 1, 2024
0b357ea
Merge branch 'main' into allow-trusted-and-privileged-plugins-to-be-f…
6543 Sep 1, 2024
8b39ef1
make no-tag mach as of now
6543 Sep 1, 2024
68560af
rename
6543 Sep 1, 2024
184a13b
make trustedClonePlugins an setting for admins
6543 Sep 1, 2024
4501f88
cli lint respect custom trusted clone plugins
6543 Sep 1, 2024
b8a62d8
Update docs/docs/30-administration/10-server-config.md
6543 Sep 1, 2024
bd6aff7
pipeline compiler: init defaultClonePlugin the same and respect 0 if …
6543 Sep 1, 2024
052668a
clean: all about trusted clone
6543 Sep 1, 2024
53a9a3f
clean docs
6543 Sep 1, 2024
c90b4de
clean docs (2)
6543 Sep 1, 2024
1710cb6
add one more info
6543 Sep 1, 2024
54f9d0a
add test
6543 Sep 1, 2024
9380b5b
Update pipeline/frontend/yaml/linter/linter_test.go
6543 Sep 1, 2024
4310ce4
Update pipeline/frontend/yaml/linter/option.go
6543 Sep 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cli/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/matrix"
pipelineLog "go.woodpecker-ci.org/woodpecker/v2/pipeline/log"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
"go.woodpecker-ci.org/woodpecker/v2/shared/utils"
)

Expand Down Expand Up @@ -185,7 +186,10 @@ func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, ax
}

// lint the yaml file
err = linter.New(linter.WithTrusted(true)).Lint([]*linter.WorkflowConfig{{
err = linter.New(
linter.WithTrusted(true),
linter.WithTrustedClonePlugins(constant.TrustedClonePlugins),
6543 marked this conversation as resolved.
Show resolved Hide resolved
).Lint([]*linter.WorkflowConfig{{
File: path.Base(file),
RawConfig: confStr,
Workflow: conf,
Expand Down
20 changes: 16 additions & 4 deletions cli/lint/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
)

// Command exports the info command.
Expand All @@ -35,6 +36,14 @@ var Command = &cli.Command{
Usage: "lint a pipeline configuration file",
ArgsUsage: "[path/to/.woodpecker.yaml]",
Action: lint,
Flags: []cli.Flag{
&cli.StringSliceFlag{
Sources: cli.EnvVars("WOODPECKER_PLUGINS_TRUSTED_CLONE"),
Name: "plugins-trusted-clone",
Usage: "Plugins witch are trusted to handle the netrc info in clone steps",
Value: constant.TrustedClonePlugins,
},
},
}

func lint(ctx context.Context, c *cli.Command) error {
Expand Down Expand Up @@ -69,7 +78,7 @@ func lintDir(ctx context.Context, c *cli.Command, dir string) error {
return nil
}

func lintFile(_ context.Context, _ *cli.Command, file string) error {
func lintFile(_ context.Context, c *cli.Command, file string) error {
fi, err := os.Open(file)
if err != nil {
return err
Expand All @@ -83,19 +92,22 @@ func lintFile(_ context.Context, _ *cli.Command, file string) error {

rawConfig := string(buf)

c, err := yaml.ParseString(rawConfig)
parsedConfig, err := yaml.ParseString(rawConfig)
if err != nil {
return err
}

config := &linter.WorkflowConfig{
File: path.Base(file),
RawConfig: rawConfig,
Workflow: c,
Workflow: parsedConfig,
}

// TODO: lint multiple files at once to allow checks for sth like "depends_on" to work
err = linter.New(linter.WithTrusted(true)).Lint([]*linter.WorkflowConfig{config})
err = linter.New(
linter.WithTrusted(true),
linter.WithTrustedClonePlugins(c.StringSlice("plugins-trusted-clone")),
).Lint([]*linter.WorkflowConfig{config})
if err != nil {
str, err := FormatLintError(config.File, err)

Expand Down
13 changes: 10 additions & 3 deletions cmd/server/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,11 @@ var flags = append([]cli.Flag{
Value: []string{"push", "pull_request"},
},
&cli.StringFlag{
Sources: cli.EnvVars("WOODPECKER_DEFAULT_CLONE_IMAGE"),
Name: "default-clone-image",
Sources: cli.EnvVars("WOODPECKER_DEFAULT_CLONE_PLUGIN", "WOODPECKER_DEFAULT_CLONE_IMAGE"),
Name: "default-clone-plugin",
Aliases: []string{"default-clone-image"},
Usage: "The default docker image to be used when cloning the repo",
Value: constant.DefaultCloneImage,
Value: constant.DefaultClonePlugin,
},
&cli.IntFlag{
Sources: cli.EnvVars("WOODPECKER_DEFAULT_PIPELINE_TIMEOUT"),
Expand All @@ -164,6 +165,12 @@ var flags = append([]cli.Flag{
Usage: "Allow plugins to run in privileged mode, if environment variable is defined but empty there will be none",
Value: constant.PrivilegedPlugins,
},
&cli.StringSliceFlag{
Sources: cli.EnvVars("WOODPECKER_PLUGINS_TRUSTED_CLONE"),
Name: "plugins-trusted-clone",
Usage: "Plugins witch are trusted to handle the netrc info in clone steps",
Value: constant.TrustedClonePlugins,
},
&cli.StringSliceFlag{
Sources: cli.EnvVars("WOODPECKER_VOLUME"),
Name: "volume",
Expand Down
6 changes: 3 additions & 3 deletions cmd/server/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/server/store"
"go.woodpecker-ci.org/woodpecker/v2/server/store/datastore"
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
)

const (
Expand Down Expand Up @@ -165,8 +164,9 @@ func setupEvilGlobals(ctx context.Context, c *cli.Command, s store.Store) error
server.Config.Pipeline.AuthenticatePublicRepos = c.Bool("authenticate-public-repos")

// Cloning
server.Config.Pipeline.DefaultCloneImage = c.String("default-clone-image")
constant.TrustedCloneImages = append(constant.TrustedCloneImages, server.Config.Pipeline.DefaultCloneImage)
6543 marked this conversation as resolved.
Show resolved Hide resolved
server.Config.Pipeline.DefaultClonePlugin = c.String("default-clone-plugin")
server.Config.Pipeline.TrustedClonePlugins = c.StringSlice("plugins-trusted-clone")
server.Config.Pipeline.TrustedClonePlugins = append(server.Config.Pipeline.TrustedClonePlugins, server.Config.Pipeline.DefaultClonePlugin)

// Execution
_events := c.StringSlice("default-cancel-previous-pipeline-events")
Expand Down
15 changes: 13 additions & 2 deletions docs/docs/30-administration/10-server-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,11 +319,13 @@ Always use authentication to clone repositories even if they are public. Needed

List of event names that will be canceled when a new pipeline for the same context (tag, branch) is created.

### `WOODPECKER_DEFAULT_CLONE_IMAGE`
### `WOODPECKER_DEFAULT_CLONE_PLUGIN`

> Default is defined in [shared/constant/constant.go](https://github.com/woodpecker-ci/woodpecker/blob/main/shared/constant/constant.go)

The default docker image to be used when cloning the repo
The default docker image to be used when cloning the repo.

It is also added to the trusted clone plugin list.

### `WOODPECKER_DEFAULT_PIPELINE_TIMEOUT`

Expand Down Expand Up @@ -352,6 +354,15 @@ a user can log into Woodpecker, without re-authentication.

Docker images to run in privileged mode. Only change if you are sure what you do!

### WOODPECKER_PLUGINS_TRUSTED_CLONE

> Defaults are defined in [shared/constant/constant.go](https://github.com/woodpecker-ci/woodpecker/blob/main/shared/constant/constant.go)

Plugins witch are trusted to handle the netrc info in clone steps.
If a clone step use an image not in this list, the netrc will not be injected and an user has to use other methods (e.g. secrets) to clone non public repos.

You should specify the tag of your images too, as this enforces exact matches.

<!--
### `WOODPECKER_VOLUME`
> Default: empty
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/91-migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Some versions need some changes to the server configuration or the pipeline conf

## `next`

- `WOODPECKER_DEFAULT_CLONE_IMAGE` got depricated use `WOODPECKER_DEFAULT_CLONE_PLUGIN`
- Check trusted-clone-plugins by image name and tag (if tag is set)
6543 marked this conversation as resolved.
Show resolved Hide resolved
- Remove `plugins/docker`, `plugins/gcr` and `plugins/ecr` from the default list of privileged plugins ([modify the list via config if needed](./30-administration/10-server-config.md#woodpecker_escalate)).
- Secret filters for plugins now check against tag if specified
- Removed `WOODPECKER_DEV_OAUTH_HOST` and `WOODPECKER_DEV_GITEA_OAUTH_URL` use `WOODPECKER_EXPERT_FORGE_OAUTH_HOST`
Expand Down
54 changes: 26 additions & 28 deletions pipeline/frontend/yaml/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,30 +92,33 @@ type ResourceLimit struct {

// Compiler compiles the yaml.
type Compiler struct {
local bool
escalated []string
prefix string
volumes []string
networks []string
env map[string]string
cloneEnv map[string]string
workspaceBase string
workspacePath string
metadata metadata.Metadata
registries []Registry
secrets map[string]Secret
reslimit ResourceLimit
defaultCloneImage string
trustedPipeline bool
netrcOnlyTrusted bool
local bool
escalated []string
prefix string
volumes []string
networks []string
env map[string]string
cloneEnv map[string]string
workspaceBase string
workspacePath string
metadata metadata.Metadata
registries []Registry
secrets map[string]Secret
reslimit ResourceLimit
defaultClonePlugin string
trustedClonePlugins []string
trustedPipeline bool
netrcOnlyTrusted bool
}

// New creates a new Compiler with options.
func New(opts ...Option) *Compiler {
compiler := &Compiler{
env: map[string]string{},
cloneEnv: map[string]string{},
secrets: map[string]Secret{},
env: map[string]string{},
cloneEnv: map[string]string{},
secrets: map[string]Secret{},
defaultClonePlugin: constant.DefaultClonePlugin,
trustedClonePlugins: constant.TrustedClonePlugins,
}
for _, opt := range opts {
opt(compiler)
Expand Down Expand Up @@ -163,20 +166,15 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
c.workspacePath = path.Clean(conf.Workspace.Path)
}

cloneImage := constant.DefaultCloneImage
if len(c.defaultCloneImage) > 0 {
cloneImage = c.defaultCloneImage
}

// add default clone step
if !c.local && len(conf.Clone.ContainerList) == 0 && !conf.SkipClone {
if !c.local && len(conf.Clone.ContainerList) == 0 && !conf.SkipClone && len(c.defaultClonePlugin) != 0 {
cloneSettings := map[string]any{"depth": "0"}
if c.metadata.Curr.Event == metadata.EventTag {
cloneSettings["tags"] = "true"
}
container := &yaml_types.Container{
Name: defaultCloneName,
Image: cloneImage,
Image: c.defaultClonePlugin,
Settings: cloneSettings,
Environment: make(map[string]any),
}
Expand Down Expand Up @@ -208,7 +206,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
}

// only inject netrc if it's a trusted repo or a trusted plugin
if !c.netrcOnlyTrusted || c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage()) {
if !c.netrcOnlyTrusted || c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) {
for k, v := range c.cloneEnv {
step.Environment[k] = v
}
Expand Down Expand Up @@ -265,7 +263,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
}

// inject netrc if it's a trusted repo or a trusted clone-plugin
if c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage()) {
if c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) {
for k, v := range c.cloneEnv {
step.Environment[k] = v
}
Expand Down
2 changes: 1 addition & 1 deletion pipeline/frontend/yaml/compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func TestCompilerCompile(t *testing.T) {
Steps: []*backend_types.Step{{
Name: "clone",
Type: backend_types.StepTypeClone,
Image: constant.DefaultCloneImage,
Image: constant.DefaultClonePlugin,
OnSuccess: true,
Failure: "fail",
Volumes: []string{defaultVolumes[0].Name + ":/woodpecker"},
Expand Down
10 changes: 8 additions & 2 deletions pipeline/frontend/yaml/compiler/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,15 @@ func WithResourceLimit(swap, mem, shmSize, cpuQuota, cpuShares int64, cpuSet str
}
}

func WithDefaultCloneImage(cloneImage string) Option {
func WithDefaultClonePlugin(cloneImage string) Option {
return func(compiler *Compiler) {
compiler.defaultCloneImage = cloneImage
compiler.defaultClonePlugin = cloneImage
}
}

func WithTrustedClonePlugins(images []string) Option {
return func(compiler *Compiler) {
compiler.trustedClonePlugins = images
}
}

Expand Down
15 changes: 12 additions & 3 deletions pipeline/frontend/yaml/compiler/option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/stretchr/testify/assert"

"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
)

func TestWithWorkspace(t *testing.T) {
Expand Down Expand Up @@ -166,9 +167,17 @@ func TestWithEnviron(t *testing.T) {
assert.Equal(t, "true", compiler.env["SHOW"])
}

func TestWithDefaultCloneImage(t *testing.T) {
func TestDefaultClonePlugin(t *testing.T) {
compiler := New(
WithDefaultCloneImage("not-an-image"),
WithDefaultClonePlugin("not-an-image"),
)
assert.Equal(t, "not-an-image", compiler.defaultCloneImage)
assert.Equal(t, "not-an-image", compiler.defaultClonePlugin)
}

func TestWithTrustedClonePlugins(t *testing.T) {
compiler := New(WithTrustedClonePlugins([]string{"not-an-image"}))
assert.ElementsMatch(t, []string{"not-an-image"}, compiler.trustedClonePlugins)

compiler = New()
assert.ElementsMatch(t, constant.TrustedClonePlugins, compiler.trustedClonePlugins)
}
33 changes: 31 additions & 2 deletions pipeline/frontend/yaml/linter/linter.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter/schema"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/types"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/utils"
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
)

// A Linter lints a pipeline configuration.
type Linter struct {
trusted bool
privilegedPlugins *[]string
trusted bool
privilegedPlugins *[]string
trustedClonePlugins *[]string
}

// New creates a new Linter with options.
Expand Down Expand Up @@ -73,6 +75,10 @@ func (l *Linter) lintFile(config *WorkflowConfig) error {
linterErr = multierr.Append(linterErr, newLinterError("Invalid or missing steps section", config.File, "steps", false))
}

if err := l.lintCloneSteps(config); err != nil {
linterErr = multierr.Append(linterErr, err)
}

if err := l.lintContainers(config, "clone"); err != nil {
linterErr = multierr.Append(linterErr, err)
}
Expand All @@ -96,6 +102,29 @@ func (l *Linter) lintFile(config *WorkflowConfig) error {
return linterErr
}

func (l *Linter) lintCloneSteps(config *WorkflowConfig) error {
if len(config.Workflow.Clone.ContainerList) == 0 {
return nil
}

trustedClonePlugins := constant.TrustedClonePlugins
if l.trustedClonePlugins != nil {
trustedClonePlugins = *l.trustedClonePlugins
}

var linterErr error
for _, container := range config.Workflow.Clone.ContainerList {
if !utils.MatchImageDynamic(container.Image, trustedClonePlugins...) {
linterErr = multierr.Append(linterErr,
newLinterError(
"Specified clone image does not match allow list, netrc will not be injected",
config.File, fmt.Sprintf("clone.%s", container.Name), true),
)
}
}
return linterErr
}

func (l *Linter) lintContainers(config *WorkflowConfig, area string) error {
var linterErr error

Expand Down
4 changes: 4 additions & 0 deletions pipeline/frontend/yaml/linter/linter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ func TestLintErrors(t *testing.T) {
from: "{steps: { build: { image: plugins/docker, settings: { test: 'true' } } }, when: { branch: main, event: push } } }",
want: "Cannot use once privileged plugins removed from WOODPECKER_ESCALATE, use 'woodpeckerci/plugin-docker-buildx' instead",
},
{
from: "{steps: { build: { image: golang, settings: { test: 'true' } } }, when: { branch: main, event: push }, clone: { git: { image: some-other/plugin-git:v1.1.0 } } }",
want: "Specified clone image does not match allow list, netrc will not be injected",
},
}

for _, test := range testdata {
Expand Down
7 changes: 7 additions & 0 deletions pipeline/frontend/yaml/linter/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,10 @@ func PrivilegedPlugins(plugins []string) Option {
linter.privilegedPlugins = &plugins
}
}

// PrivilegedPlugins adds the list of privileged plugins.
6543 marked this conversation as resolved.
Show resolved Hide resolved
func WithTrustedClonePlugins(plugins []string) Option {
return func(linter *Linter) {
linter.trustedClonePlugins = &plugins
}
}
Loading