Gitea Actions token prevents cloning **public** composite actions across owners (404 Not Found)
Gitea Version
1.27.3
What happened?
Summary
When a Gitea Actions workflow attempts to use a public composite action hosted on the same Gitea instance under a different user or organization (e.g. uses: https://git.example.com/shared-org/my-action.git@v1), the runner (act_runner) fails during the action download phase with:
Unable to clone https://git.example.com/shared-org/my-action.git refs/heads/v1: repository not found: Repository not found
Error: repository not found: Repository not found
Note: This behavior was tested on a single Gitea instance (across different users/organizations). I have not tested accessing actions from one Gitea instance to another.
The Authentication Paradox
- The repository
shared-org/my-actionis 100% public. - An anonymous user or container running
git clone https://git.example.com/shared-org/my-action.gitsucceeds without credentials (HTTP 200). - However, when executed inside a workflow via
uses:,act_runnerautomatically attaches the calling job'sGITEA_TOKENto the Git clone request. - Because the job is running in a different repository (e.g.
calling-org/my-app) andDEFAULT_ACTIONS_TOKEN_PERMISSION = restrictedis configured, Gitea restricts the token strictly to the calling repository. - When Gitea's Smart HTTP handler evaluates the request with the attached token, it detects that the token does not have scope for
shared-org/my-action, and responds with404 Not Found(to avoid leaking private repo existence). act_runner(go-git) maps that404directly torepository not found: Repository not found.
Result: Presenting a valid Gitea Actions token gives less access to a public repository than presenting no credentials at all.
This Affects Two Key Scenarios
- Scenario A — Public Instance with
DEFAULT_ACTIONS_TOKEN_PERMISSION = restricted:REQUIRE_SIGNIN_VIEW = false- The action repo is public, but cross-owner actions cannot be consumed via standard
uses:because the restricted token is rejected with404.
- Scenario B — Private Instance with
REQUIRE_SIGNIN_VIEW = true(re: #28187):- Even when an internal action repository is meant to be shared across the entire instance, workflows in different organizations cannot clone it because the automatic job token is scoped only to its own repository/organization.
Steps to Reproduce
- Configure Gitea with
DEFAULT_ACTIONS_TOKEN_PERMISSION = restrictedinapp.ini/ environment:[actions] DEFAULT_ACTIONS_TOKEN_PERMISSION = restricted - Create a public repository containing a composite action under User A (e.g.
user-a/hello-action). - In a repository under Org B (e.g.
org-b/my-app), create a workflow referencing User A's action:name: Test Action on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: https://git.example.com/user-a/hello-action.git@main - Trigger the workflow.
act_runnerwill fail to download the action, reportingrepository not found: Repository not found.
Technical Analysis & Root Cause
In
act_runner(pkg/runner/step_action_remote.go): Whenact_runnerprepares to download a remote action, it unconditionally injects the job token:gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{ URL: sar.remoteAction.CloneURL(), Token: github.Token, // Automatically injects GITEA_TOKEN })There is no workflow syntax option in
uses:to specify an anonymous clone.In Gitea HTTP Git Backend (
routers/web/repo/githttp.go&services/context/permission.go): WhenGET /<owner>/<repo>.git/info/refs?service=git-upload-packarrives:- If unauthenticated,
canAnonymousPullevaluates totruefor public repositories, and access is granted (200 OK). - If authenticated (
ctx.IsSigned == true), Gitea invokescontext.CheckRepoScopedToken(ctx, repo, ...)to enforce token scopes. - Under
restrictedmode, the Actions token only has permissions for the calling repo, failingCheckRepoScopedToken. - Gitea returns
404 Not Found, breakinggo-git's clone.
- If unauthenticated,
Working Workaround
Instead of letting act_runner attempt to clone the remote action with its injected GITEA_TOKEN, fetch the action repository into a local subfolder in a preceding step, then reference it as a local composite action (uses: ./<path>):
- name: Fetch Shared Action
run: |
rm -rf .gitea/actions/shared-action
# For public instances (Scenario A): anonymous git clone works without credentials
git clone --depth 1 --branch v1 https://git.example.com/user-a/shared-action.git .gitea/actions/shared-action
# For private instances (Scenario B): authenticate via deploy key or secret PAT
# git clone --depth 1 --branch v1 https://token:${{ secrets.SHARED_ACTION_PAT }}@git.example.com/user-a/shared-action.git .gitea/actions/shared-action
- name: Run Action
uses: ./.gitea/actions/shared-action
with:
# action inputs...
(Add .gitea/actions/ to .gitignore to keep cloned actions untracked).
Why this works:
git cloneinside arun:step bypassesact_runner's automatic token injection and clones anonymously.act_runnerreads local actions (uses: ./.gitea/actions/...) dynamically at step execution time without calling the Gitea API.
Proposed Fix
- Gitea Side: In
githttp.go(andCheckRepoScopedToken), if a repository is public and the requested action is a read-only pull (git-upload-pack), Gitea should not deny access simply because the suppliedGITEA_TOKENis scoped to a different repository. An authenticated user/token should never have fewer permissions than an anonymous guest. act_runnerSide: Support either attempting an unauthenticated clone fallback if an authenticated clone returns 404/401 on an action URL, or checking if the host is a public Git remote before injectinggithub.Token.
How are you running Gitea?
Dockerrized behind a traefik reverse proxy. Full setup with Postgres and Redis.
Source: go-gitea/gitea