Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ on:
- main

jobs:
reject-promptless-linux-drop-sudo:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Run promptless drop-sudo invocation
id: promptless
continue-on-error: true
uses: ./
with:
safety-strategy: drop-sudo

- name: Verify invocation was rejected
if: ${{ always() }}
env:
PROMPTLESS_OUTCOME: ${{ steps.promptless.outcome }}
run: test "$PROMPTLESS_OUTCOME" = failure

verify:
runs-on: ubuntu-latest
steps:
Expand All @@ -33,6 +52,9 @@ jobs:
- name: Test
run: pnpm test

- name: Test privileged Linux identity transition
run: pnpm run test:privileged

- name: Build bundles
run: pnpm run build

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# codex-action Changelog

## Unreleased

- Harden Linux `drop-sudo` by launching Codex behind a trusted gate under a distinct, locked system account with scoped filesystem ACLs, no supplementary groups or capabilities, and `no_new_privs` enabled.
- **Breaking (Linux):** `drop-sudo` now requires a prompt and supports one protected invocation per fresh job; setup-only use and later direct `codex` invocations cannot preserve the clean execution identity.

## [v1.11](https://github.com/openai/codex-action/tree/v1.11) (2026-07-04)

- [#116](https://github.com/openai/codex-action/pull/116) keep the permission profile helper backward compatible
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,16 @@ The `safety-strategy` input determines how much access Codex receives on the run

See [Protecting your `OPENAI_API_KEY`](./docs/security.md#protecting-your-openai_api_key) on the Security page for important details on this topic.

- **`drop-sudo` (default)** — On Linux and macOS runners, the action revokes the default user’s `sudo` membership before invoking Codex. Codex then runs as that user without superuser privileges. This change lasts for the rest of the job, so subsequent steps cannot rely on `sudo`. This is usually the safest choice on GitHub-hosted runners.
- **`drop-sudo` (default)** — On Linux and macOS runners, the action revokes the default user’s `sudo` authorization before releasing the prompt to Codex. On Linux, it first creates a unique locked system account, grants that account ACL access to the checkout, Codex home, schema, and output, and launches Codex with that distinct UID, one primary group, no process capabilities, and `no_new_privs`. A trusted gate prevents Codex or MCP startup from running until runner sudo has been revoked. The distinct UID also prevents Codex from signaling or attaching to runner-owned processes that retain groups such as `docker`. On macOS, Codex still runs as the default user; sudo revocation does not clear that live process's other supplementary groups. The sudo change is a host-level mutation, so subsequent steps and later jobs on the same persistent host cannot rely on that account's sudo authorization. Use this strategy on disposable runners; the distinct-identity hardening described here is currently Linux-only.
- **`unprivileged-user`** — Runs Codex as the user provided via `codex-user`. Use this if you manage your own runner with a pre-created unprivileged account. Ensure the user can read the repository checkout and any files Codex needs. See [`unprivileged-user.yml`](./examples/unprivileged-user.yml) for an example of how to configure such an account on `ubuntu-latest`.
- **`read-only`** — Executes Codex in a read-only sandbox. Codex can view files but cannot mutate the filesystem or access the network directly. The OpenAI API key still flows through the proxy, so Codex could read it if it can reach process memory.
- **`unsafe`** — No privilege reduction. Codex runs as the default `runner` user (which typically has `sudo`). Only use this when you fully trust the prompt. On Windows runners this is the only supported choice and the action will fail if another option is provided.

### Operating system support

- **Windows**: GitHub-hosted Windows runners lack a supported sandbox. Set `safety-strategy: unsafe`. The action validates this and exits early otherwise.
- **Linux/macOS**: All options for `safety-strategy` are supported. Again, if you pick `drop-sudo`, remember that later steps in your `job` that rely on `sudo` will fail. If you do need to run code that requires `sudo` after `openai/codex-action` has run, one option is to pipe the output of `openai/codex-action` to a fresh `job` on a new host and to continue your workflow from there.
- **GitHub-hosted Linux runners**: The action enables unprivileged user namespaces during setup and clears Ubuntu's AppArmor gate when present. This avoids the `bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted` failure seen on newer hosted images, including workflows that use the action once to bootstrap Codex and then call `codex` in later steps. Self-hosted Linux runners still need equivalent kernel support configured ahead of time.
- **Linux/macOS**: All options for `safety-strategy` are supported on a non-root, sudo-capable runner account. Linux container jobs that start the action as UID 0 cannot use `drop-sudo`. If you pick `drop-sudo`, later steps that rely on `sudo` will fail. Continue privileged work only in a fresh job on a new disposable host.
- **GitHub-hosted Linux runners**: The action enables unprivileged user namespaces during setup and clears Ubuntu's AppArmor gate when present. This avoids the `bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted` failure seen on newer hosted images. Linux `drop-sudo` relies on the standard Ubuntu locations for tools such as `/usr/bin/setpriv`, `/usr/bin/setfacl`, `/usr/bin/find`, and `/usr/sbin/useradd`; these are present on GitHub-hosted Ubuntu runners. Checkout and Codex home roots and the output file must be owned by the runner account. A schema outside those roots must either be runner-owned or already world-readable, and non-runner-owned parent directories must already be world-searchable. The action rejects symbolic-link path components and does not transfer ACL access through hard-linked files. The resolved Codex executable must already be a world-readable and executable regular file beneath world-searchable directories; the global installation performed by the action satisfies this on GitHub-hosted runners. Use disposable, Ubuntu-compatible self-hosted runners with that filesystem layout, or preprovision an account and select `unprivileged-user` on another distribution or a persistent host.

## Outputs

Expand All @@ -193,7 +193,7 @@ jobs:
- To use a non-default Responses endpoint (for example Azure OpenAI), set `responses-api-endpoint` to the provider's URL while keeping `openai-api-key` populated; the proxy will still send `Authorization: Bearer <key>` upstream.
- If you want Codex to have access to a narrow set of privileged functionality, consider running a local MCP server that can perform these actions and configure Codex to use it.
- If you need more control over the CLI invocation, pass flags through `codex-args` or create a `config.toml` in `codex-home`. Prefer a [permission profile](https://developers.openai.com/codex/permissions), starting with `:workspace` for workspace editing, over legacy sandbox flags for new integrations.
- Once `openai/codex-action` is run once with `openai-api-key`, you can also call `codex` from subsequent scripts in your job. (You can omit `prompt` and `prompt-file` from the action in this case.)
- On Linux, the clean execution identity used by `drop-sudo` applies only to Codex launched by the action. The action rejects omitting both `prompt` and `prompt-file` because a direct `codex` call in a later step would inherit the runner service's original supplementary groups. Because the first protected invocation permanently removes the job user's sudo authorization, run each protected Codex invocation in a fresh job rather than invoking the action twice in one job.

## Azure

Expand Down
43 changes: 24 additions & 19 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,16 @@ inputs:
Specify one of the following options (on Windows, the only supported option is `unsafe`):

* `drop-sudo` (default, IRREVERSIBLE) Drop sudo privileges (if any) from
the default user before running Codex, and run Codex as that user. This
is only supported on Linux and macOS runners. This option is
irreversible: if the default user has sudo privileges, they will be
removed permanently for the duration of the job.
the default user before releasing the prompt to Codex. On Linux, Codex
runs under a separate, per-invocation system account with access to the
checkout, Codex home, and output files, but without supplementary
groups, capabilities, or permission to gain new privileges. On macOS,
Codex runs as the default user after sudo is revoked. This is only
supported on Linux and macOS runners. This option is irreversible: if
the default user has sudo privileges, its authorization is removed from
the host. Linux requires an Ubuntu-compatible tool layout, a prompt in
the same invocation, and a fresh job for every protected invocation.
Use this strategy only on disposable runners.
* `unprivileged-user` Run Codex as the specified user specified by the
`codex-user` option (the user must already exist). Note the caller is
responsible for ensuring the specified user has the privileges it needs
Expand Down Expand Up @@ -117,6 +123,15 @@ outputs:
runs:
using: "composite"
steps:
- name: Validate Linux drop-sudo invocation
if: ${{ runner.os == 'Linux' && inputs['safety-strategy'] == 'drop-sudo' && inputs.prompt == '' && inputs['prompt-file'] == '' }}
shell: bash
run: |
echo "Linux drop-sudo requires prompt or prompt-file so Codex can be launched with a clean execution identity." >&2
echo "Calling codex directly in a later step would inherit the runner's original supplementary groups." >&2
echo "Move the Codex invocation into this action, use a fresh job for each protected invocation, or explicitly select another safety strategy." >&2
exit 1

- name: Validate Windows safety strategy
if: ${{ runner.os == 'Windows' }}
shell: bash
Expand Down Expand Up @@ -301,29 +316,19 @@ runs:
fi

- name: Drop sudo privilege, if appropriate
if: ${{ inputs['safety-strategy'] == 'drop-sudo' && inputs['openai-api-key'] != '' }}
if: ${{ runner.os == 'macOS' && inputs['safety-strategy'] == 'drop-sudo' && (inputs['openai-api-key'] != '' || inputs.prompt != '' || inputs['prompt-file'] != '') }}
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
run: |
case "${RUNNER_OS}" in
Linux)
node "$ACTION_PATH/dist/main.js" drop-sudo --user runner --group sudo
;;
macOS)
node "$ACTION_PATH/dist/main.js" drop-sudo --user runner --group admin
;;
*)
echo "Unsupported OS for drop-sudo: ${RUNNER_OS}" >&2
exit 1
;;
esac
runner_user="$(/usr/bin/id -un)"
node "$ACTION_PATH/dist/main.js" drop-sudo --user "$runner_user" --group admin

- name: Verify sudo privilege removed
if: ${{ inputs['safety-strategy'] == 'drop-sudo' && inputs['openai-api-key'] != '' }}
if: ${{ runner.os == 'macOS' && inputs['safety-strategy'] == 'drop-sudo' && (inputs['openai-api-key'] != '' || inputs.prompt != '' || inputs['prompt-file'] != '') }}
shell: bash
run: |
if sudo -n true 2>/dev/null; then
if /usr/bin/sudo -n true 2>/dev/null; then
echo "Expected sudo to be disabled, but sudo succeeded." >&2
exit 1
fi
Expand Down
Loading
Loading