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.

  1. Quickstart
  2. Proxy mode
  3. WireGuard mode
  4. Verify the source IP
  5. Pro: primary and standby
  6. Inputs and outputs
  7. Security
  8. Requirements

Quickstart

  1. In your repository (or environment), open Settings, then Secrets and variables, then Actions. Add your proxy login as the secrets PENDUSES_PROXY_USER and PENDUSES_PROXY_PASSWORD, and the proxy address as the variable PENDUSES_ENDPOINTS in the form HOST:SOCKS_PORT:HTTP_PORT. The password is shown once, so copy it straight from the message that carries it.
  2. Add the action to the job, then source the file it writes in the steps that need the dedicated address.
  3. Give the address to the owner of the destination, and check the first run.
.github/workflows/sync.yml
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: false switches to plain HTTP, sends the login in clear text, and prints a warning: use it only on a lab node. There is no ALL_PROXY: SOCKS5 is not offered.
  • The proxy port takes CONNECT only. HTTPS requests work; a plain http:// request through HTTP_PROXY is 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.

.github/workflows/deploy.yml
      - 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.

.github/workflows/db-maintenance.yml
      - 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-ip one address per endpoint, in the same order as endpoints, 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.

.github/workflows/sync.yml
      - 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

InputModeWhat it does
modebothRequired. proxy or wireguard
endpointsproxyHOST:SOCKS_PORT:HTTP_PORT with the node's DNS name as host, and on Pro the standby after a comma
proxy-tlsproxytrue (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, passwordproxyLogin of the primary endpoint. Store as secrets
standby-username, standby-passwordproxyLogin of the standby endpoint. Empty means the primary's
scopeproxyfile (default) writes a file for you to source; job exports the login to every later step and prints a warning
no-proxyproxyExtra NO_PROXY entries, separated by commas
wireguard-configwireguardThe wg-quick configuration, as a secret
standby-wireguard-configwireguardConfiguration of the standby endpoint (Pro), as a secret
allowed-ipswireguardAddresses or ranges to route through the tunnel. Replaces the AllowedIPs of the file
allow-default-routewireguardDefault false. true allows a configuration that routes everything
interfacewireguardInterface name, default penduses0
verify-urlbothURL on your allowlist that answers with the address it sees
expected-ipbothAddress expected per endpoint. The job fails on a mismatch. Needs verify-url
timeoutbothSeconds per attempt, default 10
OutputValue
modeThe mode that was used
endpointprimary or standby
observed-ipThe address the verify URL saw, when you gave one
interfaceWireGuard mode: the interface that was created
env-fileProxy 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: job gives 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 holds PostUp-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_target or 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: read on the job. The action needs no token.
  • Pin the version. @v1 follows 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 have bash and curl.
  • WireGuard mode needs sudo (GitHub-hosted runners have it) and either wireguard-tools or permission to install it with apt-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.