Webinar: WWDC 26: What's new for security, developers and IT teams
Register now
Guide

Authenticate private artifacts downloaded by a formula or cask

Petros Amoiridis

Workbrew authenticates the git operations that clone and update your private Taps, but it does not authenticate the artifacts a formula or cask downloads from its url (see How Workbrew authenticates private Taps). When a formula or cask pulls its binary from a private location, such as a GitHub Release in a private repository, that download has no credential attached and fails.

The usual symptom is a checksum mismatch on install. The file that failed the checksum is often a GitHub login or SSO page, not your binary: the download came back unauthenticated, GitHub returned its login page, and brew saved and hashed that HTML instead of the asset.

This guide shows how to attach a GitHub credential to the download. The recommended way to supply that credential on a managed fleet is a Secret Brew Configuration, so the token stays in the Workbrew Console and is never written to your Devices.

Before you start

  • A GitHub token (a personal access token or fine-grained token) with read access to the repository that holds the artifact.
  • If your GitHub organization enforces SAML SSO, the token must be SSO-authorized for the organization, or the download still fails.
  • Secret Brew Configurations require Workbrew Enterprise.

1. Store the token as a Secret Brew Configuration

Create a Secret Brew Configuration with the key HOMEBREW_GITHUB_API_TOKEN and the token as its value, following Set a Secret Brew Configuration.

Workbrew injects this value into the installs and upgrades it runs through the managed brew, so the token lives in the Console only and is never distributed to Devices on disk. For exactly where the value is and is not available, see How Secret Brew Configurations stay secret.

On a managed fleet this is the dependable source: installs run as the Workbrew Agent's own user, which has no interactively-authenticated gh session or login keychain to fall back to. The formula change in the next step also picks up a gh token or keychain credential when one happens to be present, but you should not rely on those being there across a fleet.

2. Send the token from the formula or cask

A plain github.com/.../releases/download/... URL does not carry the token, so the download needs to resolve the private asset through the authenticated GitHub API and attach the credential as a request header. Do this with a custom download strategy rather than in the url line itself, so that the formula or cask keeps a normal releases/download URL and the GitHub API is only contacted at download time, not every time the formula or cask is loaded.

Define the strategy once:

# Downloads release assets from private GitHub repositories using the
# standard `releases/download` URL, resolving the API asset URL only at
# download time so that loading the formula or cask makes no GitHub API calls.
class GitHubReleaseAssetDownloadStrategy < CurlDownloadStrategy
  def initialize(url, name, version, **meta)
    super
    match = url.match %r{^https://github\.com/([^/]+)/([^/]+)/releases/download/([^/]+)/([^/]+)$}
    raise CurlDownloadStrategyError.new(url, "Not a GitHub release asset URL.") unless match

    @owner, @repo, @tag, @filename = match.captures
  end

  private

  def asset
    @asset ||= begin
      release = GitHub::API.open_rest(
        GitHub.url_to("repos", @owner, @repo, "releases", "tags", @tag),
        scopes: ["repo"],
      )

      asset = release.fetch("assets").find { |a| a.fetch("name") == @filename }
      raise CurlDownloadStrategyError.new(url, "No release asset named #{@filename} found.") unless asset

      asset
    end
  end

  def resolve_url_basename_time_file_size(_url, timeout: nil)
    [
      asset.fetch("url"),
      @filename,
      Time.parse(asset.fetch("updated_at")),
      asset.fetch("size"),
      asset.fetch("content_type"),
      false,
    ]
  end

  def _fetch(url:, resolved_url:, timeout:)
    token = GitHub::API.credentials
    header = ["Accept: application/octet-stream"]
    header << "Authorization: Bearer #{token}" unless token.empty?
    curl_download(resolved_url, to: temporary_path, try_partial: @try_partial, timeout:, header:)
  end
end

Then point the cask or formula at it with using:, keeping a standard release URL that embeds only version:

cask "your-app" do
  version "1.2.0"
  sha256 "..."

  url "https://github.com/your-org/your-private-repo/releases/download/#{version}/your-app-#{version}.zip",
      using: GitHubReleaseAssetDownloadStrategy

  # ...
end

A formula uses the same strategy. Declare version before url so the interpolation resolves:

class YourApp < Formula
  version "1.2.0"
  sha256 "..."

  url "https://github.com/your-org/your-private-repo/releases/download/#{version}/your-app-#{version}.tar.gz",
      using: GitHubReleaseAssetDownloadStrategy

  def install
    # ...
  end
end
  • The URL embeds only version, so publishing a new release means changing only version and sha256. Adjust the tag and file name segments to match how your releases are tagged and how your assets are named.
  • The strategy resolves the asset and adds the credential at download time, so loading the formula or cask makes no GitHub API calls.
  • GitHub::API.credentials returns the first GitHub credential it finds: the HOMEBREW_GITHUB_API_TOKEN you set in step 1, or a gh CLI or keychain credential if one is present. When none is available the request is unauthenticated and the private asset fails to download.
  • For several private packages, define GitHubReleaseAssetDownloadStrategy once in a shared file in your Tap and reuse it, rather than repeating it in each formula or cask.

GitHub::API.credentials is an internal Homebrew helper rather than a stable public API, so treat it as something that could change. The only guaranteed-public input is the HOMEBREW_GITHUB_API_TOKEN environment variable itself, which you can read directly with ENV["HOMEBREW_GITHUB_API_TOKEN"] if you prefer to depend only on the value you set in step 1.

3. Install through the Workbrew-managed brew

The token is only injected when the install runs through the Workbrew-managed brew at /opt/workbrew/bin/brew, which routes install, upgrade, and reinstall through the Workbrew Agent, and when Workbrew auto-installs an enabled Tap on your fleet.

Running /opt/homebrew/bin/brew directly bypasses Workbrew and gets no token, so a test there keeps failing the same way. Confirm which brew you are calling with which brew, then run it through the Workbrew-managed brew or let the enabled Tap install on the Device.

If which brew does not point where you expect, you may have a conflicting Homebrew wrapper configuration. See How to fix conflicting Homebrew wrapper configuration errors.

We use cookies to analyze traffic and improve your experience. You can accept all cookies or decline non-essential ones. Read our Privacy Policy for details.