Skip to content
Latchkey

twine "403 Forbidden" uploading to a private PyPI in CI

twine got 403 uploading to the private index. Either no credentials reached the server, or the authenticated account is not allowed to upload to that project. 403 will not clear on retry.

What this error means

twine upload fails with "HTTPError: 403 Forbidden from https://<host>/ Invalid or non-existent authentication information" or a permission message for the project.

twine
Uploading distributions to https://pypi.internal.example.com/
Uploading internal_lib-1.1.0-py3-none-any.whl
HTTPError: 403 Forbidden from https://pypi.internal.example.com/
Invalid or non-existent authentication information.

Common causes

Missing or wrong upload credentials

twine sent no username/password (or __token__ + API token), or the values are wrong, so the server forbids the upload.

The account cannot upload to that project

The credentials authenticate but the user is not an owner/maintainer of the project on the private index.

How to fix it

Supply token credentials via environment

  1. Set TWINE_USERNAME=__token__ and TWINE_PASSWORD to the API token from a secret.
  2. Point TWINE_REPOSITORY_URL at the private index.
  3. Re-run the upload.
.github/workflows/ci.yml
env:
  TWINE_USERNAME: __token__
  TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
  TWINE_REPOSITORY_URL: https://pypi.internal.example.com/

Confirm upload permission for the project

A persistent 403 with valid auth means the account is not a maintainer of the project; grant it on the index.

How to prevent it

  • Use API tokens with __token__ rather than passwords.
  • Keep credentials in CI secrets and pass them via TWINE_* env.
  • Grant the CI account maintainer rights on the target project.

Related guides

Tired of flaky CI? Latchkey auto-heals failed jobs and retries them for you. Start free →