Docs
GitHub Action
Send a job's traffic out through your dedicated IPv4 address from GitHub Actions. The action sets up the proxy or the WireGuard tunnel, can fall back to a standby endpoint, and can fail the job when the address the destination sees is not the one you expect.
Not published yet
The action is finished but not yet published, so uses: wegvon/penduses-egress@v1 does not resolve yet. Until it is, use the shell steps in the static egress quickstart, which do the same thing. We will announce it here and by email to pilot customers.
- Quickstart
- Proxy mode
- WireGuard mode
- Verify the source IP
- Pro: primary and standby
- Inputs and outputs
- Security
- Requirements
Quickstart
- In your repository (or environment), open Settings, then Secrets and variables, then Actions. Add your proxy login as the secrets
PENDUSES_PROXY_USERandPENDUSES_PROXY_PASSWORD, and the proxy address as the variablePENDUSES_ENDPOINTSin the formHOST:SOCKS_PORT:HTTP_PORT. The password is shown once, so copy it straight from the message that carries it. - Add the action to the job, then source the file it writes in the steps that need the dedicated address.
- Give the address to the owner of the destination, and check the first run.
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Prepare the dedicated IP
id: egress
uses: wegvon/penduses-egress@v1
with:
mode: proxy
endpoints: ${{ vars.PENDUSES_ENDPOINTS }}
username: ${{ secrets.PENDUSES_PROXY_USER }}
password: ${{ secrets.PENDUSES_PROXY_PASSWORD }}
# Fail the job when the destination does not see this address
verify-url: https://ip-echo.partner.example/ip
expected-ip: 203.0.113.25
- name: Call the partner API from the dedicated IP
run: |
. "${{ steps.egress.outputs.env-file }}"
curl -sS --fail https://api.partner.example/v1/sync
The address 203.0.113.25 is an example from a documentation range. Use your own, and use a verify-url that is on your allowlist and answers with the address it sees.
Proxy mode
The action tries your endpoint before anything else uses it. It sends a request through the proxy with your login, so a wrong password, a stopped proxy or an unreachable host stops the job at this step and not halfway through a deployment. Then it writes HTTPS_PROXY and HTTP_PROXY (and the lower-case names), as an https:// URL that holds your login, and a NO_PROXY that keeps localhost off the proxy, to a file that only the runner user can read. You source that file in the steps that need the dedicated address. Add more entries with the no-proxy input.
- Only the steps that source the file use the proxy. Checkout, dependency installs, uploads and every third-party action keep their direct connection, so their destinations need no place on your allowlist, and none of them receives your proxy login in its environment.
- The connection to the proxy is TLS. Use the node's DNS name in
endpoints, as in your welcome email, because the runner checks the certificate against it. A certificate that does not verify stops the job before any credential is sent.proxy-tls: falseswitches to plain HTTP, sends the login in clear text, and prints a warning: use it only on a lab node. There is noALL_PROXY: SOCKS5 is not offered. - The proxy port takes
CONNECTonly. HTTPS requests work; a plainhttp://request throughHTTP_PROXYis refused. Use HTTPS destinations. - Tools that ignore the variables, such as
psql, need WireGuard mode.
Scope: a file for the steps that need it, or the whole job
The default, scope: file, exports nothing. The action writes the settings to a file that only the runner user can read, and returns its path in the env-file output. Source the file in the steps that need the dedicated address. The rest of the job keeps its direct connection.
scope: job is the opt-in for a job in which every later step is your own code. It exports the settings, login included, to every following step, third-party actions and package scripts among them. Masking only hides the login in the log: a compromised later step can read it from its environment and use your dedicated address. The action prints a warning whenever scope is job. Put the steps that must not see the login before the action.
- name: Prepare the dedicated IP
id: egress
uses: wegvon/penduses-egress@v1
with:
mode: proxy
# scope: file is the default; shown here because it is the point of this example
scope: file
endpoints: ${{ vars.PENDUSES_ENDPOINTS }}
username: ${{ secrets.PENDUSES_PROXY_USER }}
password: ${{ secrets.PENDUSES_PROXY_PASSWORD }}
- name: Run the migration from the dedicated IP
run: |
. "${{ steps.egress.outputs.env-file }}"
curl -sS --fail https://api.partner.example/v1/migrate
- name: Build (direct connection)
run: make build
WireGuard mode
Use it for clients that do not speak HTTP proxies, such as psql. Store the whole configuration file we sent you as a secret. The action installs wireguard-tools if the runner does not have them, brings the interface up, waits for the handshake, and takes the interface down when the job ends, also when the job fails or is cancelled.
Give allowed-ips the destinations to send through the tunnel. They replace the AllowedIPs of the file, so only those addresses leave from your dedicated address and the rest of the job keeps its direct route. The action refuses a configuration that routes everything (0.0.0.0/0), because your allowlist would block every other destination of the job. Set allow-default-route: true only if you mean it. It also ignores DNS lines, so the runner keeps its resolver.
- name: Bring up the tunnel to the database
uses: wegvon/penduses-egress@v1
with:
mode: wireguard
wireguard-config: ${{ secrets.PENDUSES_WG_CONFIG }}
# The database host, as on your allowlist
allowed-ips: 203.0.113.10/32
- name: Vacuum
env:
PGPASSWORD: ${{ secrets.DB_PASSWORD }}
run: psql "host=203.0.113.10 dbname=app user=app sslmode=require" -c "VACUUM (ANALYZE)"
WireGuard mode routes addresses, not host names, and the allowlist for it is address ranges with ports. See Allowlist.
Verify the source IP
Give verify-url a URL on your allowlist that answers with the address it sees (plain text or JSON; the action takes the first IPv4 address in the answer). Add expected-ip and the job fails, before the proxy settings are exported, when the address is a different one. The address is also returned in the observed-ip output.
- Without
verify-url, proxy mode only checks that the proxy is reachable and accepts your login. - On Pro, give
expected-ipone address per endpoint, in the same order asendpoints, separated by commas. - The action does not print your login, your key or the full configuration. A wrong address is reported as an address, nothing more.
Pro: primary and standby
Pro gives you an address in France (GRA) and one in Germany (LIM). Give both endpoints, primary first. The action tries the primary and, when it is not usable, the standby, and says which it took in a warning and in the endpoint output. Each endpoint has its own login: pass the second as standby-username and standby-password. In WireGuard mode pass standby-wireguard-config.
- name: Route the job through the dedicated IP
id: egress
uses: wegvon/penduses-egress@v1
with:
mode: proxy
# Primary first, standby after the comma
endpoints: ${{ vars.PENDUSES_ENDPOINTS }}
username: ${{ secrets.PENDUSES_GRA_USER }}
password: ${{ secrets.PENDUSES_GRA_PASSWORD }}
standby-username: ${{ secrets.PENDUSES_LIM_USER }}
standby-password: ${{ secrets.PENDUSES_LIM_PASSWORD }}
verify-url: https://ip-echo.partner.example/ip
# One address per endpoint, in the same order
expected-ip: 203.0.113.25,192.0.2.25
The choice is made once, when the step runs, on your side. We do not switch traffic for you, there is no automatic failover, and the pilot has no SLA. The destination must allowlist both addresses. More in Pro failover.
Inputs and outputs
| Input | Mode | What it does |
|---|---|---|
mode | both | Required. proxy or wireguard |
endpoints | proxy | HOST:SOCKS_PORT:HTTP_PORT with the node's DNS name as host, and on Pro the standby after a comma |
proxy-tls | proxy | true (default): TLS to the proxy, certificate checked against the host name. false: plain HTTP with the login in clear text, for a lab node only |
username, password | proxy | Login of the primary endpoint. Store as secrets |
standby-username, standby-password | proxy | Login of the standby endpoint. Empty means the primary's |
scope | proxy | file (default) writes a file for you to source; job exports the login to every later step and prints a warning |
no-proxy | proxy | Extra NO_PROXY entries, separated by commas |
wireguard-config | wireguard | The wg-quick configuration, as a secret |
standby-wireguard-config | wireguard | Configuration of the standby endpoint (Pro), as a secret |
allowed-ips | wireguard | Addresses or ranges to route through the tunnel. Replaces the AllowedIPs of the file |
allow-default-route | wireguard | Default false. true allows a configuration that routes everything |
interface | wireguard | Interface name, default penduses0 |
verify-url | both | URL on your allowlist that answers with the address it sees |
expected-ip | both | Address expected per endpoint. The job fails on a mismatch. Needs verify-url |
timeout | both | Seconds per attempt, default 10 |
| Output | Value |
|---|---|
mode | The mode that was used |
endpoint | primary or standby |
observed-ip | The address the verify URL saw, when you gave one |
interface | WireGuard mode: the interface that was created |
env-file | Proxy mode with scope: file: the file to source |
Security
- Secrets are masked. The action registers your password, your key and the URLs built from them with the runner's log masker before it does anything else. It does not echo an input, passes credentials to tools through a pipe and not on a command line, and switches shell tracing off.
- No quiet fallback to a direct connection. If the endpoints fail, the job fails. The only fallback is the standby you configured, and it is announced.
- The login stays out of other steps. By default (
scope: file) only the steps that source the file receive your proxy login.scope: jobgives it to every later step, third-party actions included, and prints a warning. The action also keeps your secrets and the runner's tokens out of the environment of the programs it starts, and refuses a WireGuard configuration that holdsPostUp-style hooks (PreUp,PostUp,PreDown,PostDown,SaveConfig,Table). - Forks and untrusted code. Secrets are not passed to workflows triggered by pull requests from forks, and you should keep it so. Do not use
pull_request_targetor a similar trigger to run code from a fork with your egress secrets: that code could use your dedicated address. - Least privilege. Set
permissions: contents: readon the job. The action needs no token. - Pin the version.
@v1follows updates. For production, pin the full commit SHA of a release you reviewed, and update it deliberately. - Rotate after exposure. If a secret appears in a log, a fork or a screenshot, treat it as lost and ask us to rotate it. See Security notes.
Requirements
- A Linux runner, such as
ubuntu-latest. Self-hosted runners must be recent enough to run Node 24 actions and havebashandcurl. - WireGuard mode needs
sudo(GitHub-hosted runners have it) and eitherwireguard-toolsor permission to install it withapt-get. - Proxy mode needs the runner to reach your proxy address and ports. WireGuard mode needs outbound UDP to the endpoint.
The pilot is best effort, with no SLA. Questions: support@penduses.com.