2 Commits

Author SHA1 Message Date
Renovate Bot
99e3e9d1f8 fix(deps): update module github.com/docker/cli to v29.6.1+incompatible 2026-07-01 00:15:06 +00:00
bircni
745b0ab6e4 fix: attach task token when cloning actions from self-hosted instance on a different host (#1056)
When `DEFAULT_ACTIONS_URL=self`, action clone URLs (`uses: owner/repo@ref`) are
built from the Gitea **AppURL** (`gitea_default_actions_url`), but
`shouldCloneURLUseToken` compared the clone URL host only against the runner's
**registered address** (`GitHubInstance`).

When the runner registers with a different hostname than AppURL — same instance,
different DNS (e.g. `gitea.local` vs `gitea.my-nas.lan`, internal vs external) —
the strict `u1.Host == u2.Host` check returns false, so the task token is **not**
attached and the action clone goes out anonymously. Against an instance with
`REQUIRE_SIGNIN_VIEW=true` this fails with:

```
Unable to clone https://gitea.example/owner/action refs/heads/v1: authentication required
```

The current workaround is to make the runner's registered host exactly match
`AppURL`. This PR removes the need for that.

Refs: https://github.com/go-gitea/gitea/issues/27933

## Change

- `shouldCloneURLUseToken` now trusts the clone URL when its host matches **either**
  the registered instance (`GitHubInstance`) **or** the self-hosted default-actions
  instance (`DefaultActionInstance`). Embedded basic auth is still rejected, and the
  empty-host cases are unchanged.
- A new `Config.DefaultActionInstanceIsSelfHosted` flag gates the second candidate.
  It is set in the daemon layer (`run/runner.go`, `exec.go`), where `github.com` and
  a configured `GithubMirror` are distinguishable, so the token is **never** attached
  for off-instance hosts.Reviewed-on: https://gitea.com/gitea/runner/pulls/1056
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
2026-06-30 16:18:12 +00:00
8 changed files with 128 additions and 50 deletions

View File

@@ -305,30 +305,45 @@ func setReusedWorkflowCallerResult(rc *RunContext, runner Runner) common.Executo
// getGitCloneToken returns GITEA_TOKEN when shouldCloneURLUseToken returns true, // getGitCloneToken returns GITEA_TOKEN when shouldCloneURLUseToken returns true,
// otherwise returns an empty string // otherwise returns an empty string
func getGitCloneToken(conf *Config, cloneURL string) string { func getGitCloneToken(conf *Config, cloneURL string) string {
if !shouldCloneURLUseToken(conf.GitHubInstance, cloneURL) { if !shouldCloneURLUseToken(conf.GitHubInstance, conf.trustedActionInstance(), cloneURL) {
return "" return ""
} }
return conf.GetToken() return conf.GetToken()
} }
// For Gitea // For Gitea
// shouldCloneURLUseToken returns true when the following conditions are met: // trustedActionInstance returns the self-hosted DEFAULT_ACTIONS_URL host that may carry the
// 1. cloneURL is from the same Gitea instance that the runner is registered to // task token, or "" when actions resolve to github.com / a GithubMirror (never trusted).
// 2. the cloneURL does not have basic auth embedded func (c Config) trustedActionInstance() string {
func shouldCloneURLUseToken(instanceURL, cloneURL string) bool { if c.DefaultActionInstanceIsSelfHosted {
if !strings.HasPrefix(instanceURL, "http://") && return c.DefaultActionInstance
!strings.HasPrefix(instanceURL, "https://") {
instanceURL = "https://" + instanceURL
} }
return ""
u1, err1 := url.Parse(instanceURL) }
u2, err2 := url.Parse(cloneURL)
if err1 != nil || err2 != nil { // For Gitea
return false // shouldCloneURLUseToken returns true when the following conditions are met:
} // 1. cloneURL's host matches this Gitea instance: either the registered instance
if u2.User != nil { // (instanceURL) or, for DEFAULT_ACTIONS_URL=self on a different hostname, the
return false // self-hosted action instance (trustedActionInstance, "" when not trusted)
} // 2. the cloneURL does not have basic auth embedded
func shouldCloneURLUseToken(instanceURL, trustedActionInstance, cloneURL string) bool {
return u1.Host == u2.Host u2, err := url.Parse(cloneURL)
if err != nil || u2.User != nil {
return false
}
for _, candidate := range []string{instanceURL, trustedActionInstance} {
if candidate == "" {
continue
}
if !strings.HasPrefix(candidate, "http://") &&
!strings.HasPrefix(candidate, "https://") {
candidate = "https://" + candidate
}
if u1, err := url.Parse(candidate); err == nil && u1.Host == u2.Host {
return true
}
}
return false
} }

View File

@@ -136,12 +136,30 @@ func TestGetGitCloneTokenWithSchemalessGiteaInstance(t *testing.T) {
require.Equal(t, "token-value", token) require.Equal(t, "token-value", token)
} }
func TestGetGitCloneTokenSelfHostedActionsDifferentHost(t *testing.T) {
// The runner registered with one hostname while DEFAULT_ACTIONS_URL=self resolves
// actions against AppURL on a different hostname for the same instance.
conf := &Config{
GitHubInstance: "gitea.local",
DefaultActionInstance: "https://gitea.my-nas.lan",
DefaultActionInstanceIsSelfHosted: true,
Secrets: map[string]string{
"GITEA_TOKEN": "token-value",
},
}
token := getGitCloneToken(conf, "https://gitea.my-nas.lan/owner/action")
require.Equal(t, "token-value", token)
}
func TestShouldCloneURLUseToken(t *testing.T) { func TestShouldCloneURLUseToken(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
instanceURL string instanceURL string
cloneURL string trustedActionInstance string
want bool cloneURL string
want bool
}{ }{
{ {
name: "same host with schemaless instance", name: "same host with schemaless instance",
@@ -173,11 +191,37 @@ func TestShouldCloneURLUseToken(t *testing.T) {
cloneURL: "://gitea.example.net/actions/tools", cloneURL: "://gitea.example.net/actions/tools",
want: false, want: false,
}, },
{
// self-hosted DEFAULT_ACTIONS_URL on a different hostname than the
// registered instance: the token must still be attached.
name: "self-hosted action instance on different host",
instanceURL: "gitea.local",
trustedActionInstance: "https://gitea.my-nas.lan",
cloneURL: "https://gitea.my-nas.lan/owner/action",
want: true,
},
{
// embedded basic auth must still be rejected even when the host matches
// the trusted action instance.
name: "self-hosted action instance with embedded basic auth",
instanceURL: "gitea.local",
trustedActionInstance: "https://gitea.my-nas.lan",
cloneURL: "https://user:pass@gitea.my-nas.lan/owner/action",
want: false,
},
{
// github.com / mirror hosts are never trusted: trustedActionInstance is
// empty in github mode, so an off-instance clone URL gets no token.
name: "github mode does not trust mirror host",
instanceURL: "gitea.local",
cloneURL: "https://mirror.example.com/owner/action",
want: false,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, shouldCloneURLUseToken(tt.instanceURL, tt.cloneURL)) require.Equal(t, tt.want, shouldCloneURLUseToken(tt.instanceURL, tt.trustedActionInstance, tt.cloneURL))
}) })
} }
} }

View File

@@ -73,18 +73,24 @@ type Config struct {
ContainerNetworkCreateOptions container.NewDockerNetworkCreateExecutorInput // the default network create options ContainerNetworkCreateOptions container.NewDockerNetworkCreateExecutorInput // the default network create options
ActionCache ActionCache // Use a custom ActionCache Implementation ActionCache ActionCache // Use a custom ActionCache Implementation
PresetGitHubContext *model.GithubContext // the preset github context, overrides some fields like DefaultBranch, Env, Secrets etc. PresetGitHubContext *model.GithubContext // the preset github context, overrides some fields like DefaultBranch, Env, Secrets etc.
EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath
ContainerNamePrefix string // the prefix of container name ContainerNamePrefix string // the prefix of container name
ContainerMaxLifetime time.Duration // the max lifetime of job containers ContainerMaxLifetime time.Duration // the max lifetime of job containers
CleanWorkdir bool // remove host executor workdir on teardown CleanWorkdir bool // remove host executor workdir on teardown
DefaultActionInstance string // the default actions web site DefaultActionInstance string // the default actions web site
PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil // DefaultActionInstanceIsSelfHosted reports whether DefaultActionInstance is this
JobLoggerLevel *log.Level // the level of job logger // self-hosted Gitea (DEFAULT_ACTIONS_URL=self). It gates token trust: only then may the
ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers // task token be attached to action clone URLs on DefaultActionInstance's host, which can
InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance // differ from GitHubInstance when the runner registered with a different hostname than
MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count) // AppURL. It is never set for github.com or a GithubMirror, so the token stays on-instance.
AllocatePTY bool // allocate a pseudo-TTY for each step's process DefaultActionInstanceIsSelfHosted bool
PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil
JobLoggerLevel *log.Level // the level of job logger
ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers
InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance
MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count)
AllocatePTY bool // allocate a pseudo-TTY for each step's process
} }
// GetToken: Adapt to Gitea // GetToken: Adapt to Gitea

View File

@@ -121,7 +121,7 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
// top-level token; keep the shouldCloneURLUseToken host gate to avoid leaking it. // top-level token; keep the shouldCloneURLUseToken host gate to avoid leaking it.
cloneURL := sar.remoteAction.CloneURL(defaultActionURL) cloneURL := sar.remoteAction.CloneURL(defaultActionURL)
token := "" token := ""
if shouldCloneURLUseToken(sar.RunContext.Config.GitHubInstance, cloneURL) { if shouldCloneURLUseToken(sar.RunContext.Config.GitHubInstance, sar.RunContext.Config.trustedActionInstance(), cloneURL) {
token = github.Token token = github.Token
} }
gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{ gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{

2
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/containerd/errdefs v1.0.0 github.com/containerd/errdefs v1.0.0
github.com/creack/pty v1.1.24 github.com/creack/pty v1.1.24
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli v29.6.0+incompatible github.com/docker/cli v29.6.1+incompatible
github.com/docker/go-connections v0.7.0 github.com/docker/go-connections v0.7.0
github.com/go-git/go-billy/v5 v5.9.0 github.com/go-git/go-billy/v5 v5.9.0
github.com/go-git/go-git/v5 v5.19.1 github.com/go-git/go-git/v5 v5.19.1

2
go.sum
View File

@@ -51,6 +51,8 @@ github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvr
github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.6.0+incompatible h1:nw9himxMMZ7eIeherJNlKQq+acnlzGgHd+4uf10QRSc= github.com/docker/cli v29.6.0+incompatible h1:nw9himxMMZ7eIeherJNlKQq+acnlzGgHd+4uf10QRSc=
github.com/docker/cli v29.6.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.6.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.6.1+incompatible h1:oO7F4nn3Ovr/5TlfTUWFbMwBSS/B7Xs6Epv26gBrUP8=
github.com/docker/cli v29.6.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker-credential-helpers v0.9.6 h1:cT2PbRPSlnMmNTfT2TDMXRyQ1KMWHG7xoTLBcn1ZNv0= github.com/docker/docker-credential-helpers v0.9.6 h1:cT2PbRPSlnMmNTfT2TDMXRyQ1KMWHG7xoTLBcn1ZNv0=
github.com/docker/docker-credential-helpers v0.9.6/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/docker-credential-helpers v0.9.6/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=

View File

@@ -443,10 +443,11 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
NoSkipCheckout: execArgs.noSkipCheckout, NoSkipCheckout: execArgs.noSkipCheckout,
// PresetGitHubContext: preset, // PresetGitHubContext: preset,
// EventJSON: string(eventJSON), // EventJSON: string(eventJSON),
ContainerNamePrefix: "GITEA-ACTIONS-TASK-" + eventName, ContainerNamePrefix: "GITEA-ACTIONS-TASK-" + eventName,
ContainerMaxLifetime: maxLifetime, ContainerMaxLifetime: maxLifetime,
ContainerNetworkMode: container.NetworkMode(execArgs.network), ContainerNetworkMode: container.NetworkMode(execArgs.network),
DefaultActionInstance: execArgs.defaultActionsURL, DefaultActionInstance: execArgs.defaultActionsURL,
DefaultActionInstanceIsSelfHosted: execArgs.defaultActionsURL != "" && execArgs.defaultActionsURL != "https://github.com",
PlatformPicker: func(_ []string) string { PlatformPicker: func(_ []string) string {
return execArgs.image return execArgs.image
}, },

View File

@@ -299,6 +299,15 @@ func (r *Runner) getDefaultActionsURL(task *runnerv1.Task) string {
return giteaDefaultActionsURL return giteaDefaultActionsURL
} }
// isSelfHostedActionsURL reports whether actions resolve to this self-hosted Gitea
// (DEFAULT_ACTIONS_URL=self), i.e. gitea_default_actions_url is AppURL rather than
// github.com (which may be mirror-substituted by getDefaultActionsURL). Only then may the
// task token be attached to action clone URLs on the actions instance host.
func (r *Runner) isSelfHostedActionsURL(task *runnerv1.Task) bool {
giteaDefaultActionsURL := task.Context.Fields["gitea_default_actions_url"].GetStringValue()
return giteaDefaultActionsURL != "" && giteaDefaultActionsURL != "https://github.com"
}
func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.Reporter) (err error) { func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.Reporter) (err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -446,14 +455,15 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
EnableIPv4: r.cfg.Container.NetworkCreateOptions.EnableIPv4, EnableIPv4: r.cfg.Container.NetworkCreateOptions.EnableIPv4,
EnableIPv6: r.cfg.Container.NetworkCreateOptions.EnableIPv6, EnableIPv6: r.cfg.Container.NetworkCreateOptions.EnableIPv6,
}, },
ContainerOptions: r.cfg.Container.Options, ContainerOptions: r.cfg.Container.Options,
ContainerDaemonSocket: r.cfg.Container.DockerHost, ContainerDaemonSocket: r.cfg.Container.DockerHost,
Privileged: r.cfg.Container.Privileged, Privileged: r.cfg.Container.Privileged,
DefaultActionInstance: r.getDefaultActionsURL(task), DefaultActionInstance: r.getDefaultActionsURL(task),
PlatformPicker: r.labels.PickPlatform, DefaultActionInstanceIsSelfHosted: r.isSelfHostedActionsURL(task),
Vars: task.Vars, PlatformPicker: r.labels.PickPlatform,
ValidVolumes: r.cfg.Container.ValidVolumes, Vars: task.Vars,
InsecureSkipTLS: r.cfg.Runner.Insecure, ValidVolumes: r.cfg.Container.ValidVolumes,
InsecureSkipTLS: r.cfg.Runner.Insecure,
} }
rr, err := runner.New(runnerConfig) rr, err := runner.New(runnerConfig)