{"id":15472252,"url":"https://github.com/sethmlarson/secure-python-package-template","last_synced_at":"2025-04-06T16:12:11.311Z","repository":{"id":44148838,"uuid":"74395576","full_name":"sethmlarson/secure-python-package-template","owner":"sethmlarson","description":"Template for a Python package with a secure project host and package repository configuration.","archived":false,"fork":false,"pushed_at":"2023-11-28T23:55:29.000Z","size":138,"stargazers_count":189,"open_issues_count":9,"forks_count":11,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-30T15:09:16.683Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://sethmlarson.dev/blog/security-for-package-maintainers","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sethmlarson.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-11-21T18:55:16.000Z","updated_at":"2025-03-13T14:45:33.000Z","dependencies_parsed_at":"2024-10-26T20:41:57.732Z","dependency_job_id":null,"html_url":"https://github.com/sethmlarson/secure-python-package-template","commit_stats":{"total_commits":79,"total_committers":4,"mean_commits":19.75,"dds":0.5443037974683544,"last_synced_commit":"1e05186381a52c686246743684df699477da88a3"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sethmlarson%2Fsecure-python-package-template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sethmlarson%2Fsecure-python-package-template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sethmlarson%2Fsecure-python-package-template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sethmlarson%2Fsecure-python-package-template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sethmlarson","download_url":"https://codeload.github.com/sethmlarson/secure-python-package-template/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247509225,"owners_count":20950232,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-02T02:29:50.544Z","updated_at":"2025-04-06T16:12:11.286Z","avatar_url":"https://github.com/sethmlarson.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Secure Python package template\n\n[![SLSA level 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev)\n[![OpenSSF Scorecards](https://api.securityscorecards.dev/projects/github.com/sethmlarson/secure-python-package-template/badge)](https://deps.dev/pypi/secure-package-template)\n\nTemplate for a Python package with a secure\nproject host and package repository configuration.\n\nThe goals of this project are to:\n\n- Show how to configure a Python package hosted on GitHub with:\n  - Operational security best-practices\n  - Automated publishing to PyPI\n  - Code quality and vulnerability scanning\n  - Build reproducibility\n  - Releases with provenance attestation\n- Obtain a perfect rating from [OpenSSF Scorecard](https://github.com/ossf/scorecard)\n- [SLSA Level 3](https://slsa.dev) using GitHub OIDC\n\n## Configuring git for commit and tag signing\n\n\u003e **Info**\n\u003e Commit and tag signing is a practice that's recommended to avoid commit author spoofing\n\u003e but isn't strictly required for a secure project configuration.\n\u003e If you'd like to skip this step, you can jump ahead to [creating a GitHub repository](https://github.com/sethmlarson/secure-python-package-template/#creating-the-github-repository).\n\nGit needs to be configured to be able to sign commits and tags. Git uses GPG for signing, so you need to\n[create a GPG key](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key)\nif you don't have one already. Make sure you use a [email address associated with your GitHub account](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address)\nas the email address for the key. If you wish to keep your email address private you should use GitHub's provided `noreply` email address.\n\n```sh\ngpg --full-generate-key\n```\n\nAfter you've generated a GPG key you need to [add the GPG key to your GitHub account](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account).\nThen locally you can [configure git to use your signing key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key):\n\n```sh\ngit config --global --unset gpg.format\n```\n\nList GPG secret keys, in this example the key ID is '3AA5C34371567BD2'\n\n```sh\n$ gpg --list-secret-keys --keyid-format=long\n/Users/hubot/.gnupg/secring.gpg\n------------------------------------\nsec   4096R/3AA5C34371567BD2 2016-03-10 [expires: 2017-03-10]\nuid                          Hubot \u003chubot@example.com\u003e\nssb   4096R/4BB6D45482678BE3 2016-03-10\n```\n\nTell git about your signing key:\n\n```sh\ngit config --global user.signingkey 3AA5C34371567BD2\n````\n\nThen tell git to auto-sign commits and tags:\n\n```sh\ngit config --global commit.gpgsign true\ngit config --global tag.gpgSign true\n```\n\nNow all commits and tags you create from this git instances will be signed and show up as \"verified\" on GitHub.\n\n## Creating the GitHub repository\n\nClone this repository locally:\n\n```sh\ngit clone ssh://git@github.com/sethmlarson/secure-python-package-template\n```\n\nRename the folder to the name of the package and remove existing git repository:\n\n```sh\nmv secure-python-package-template package-name\ncd package-name\nrm -rf .git\n```\n\nCreate a new git repository and ensure the branch name is `main`:\n\n```sh\n$ git init\nInitialized empty Git repository in .../package-name/.git/\n\n$ git status\nOn branch main\n\nNo commits yet\n...\n```\n\nIf the branch isn't named `main` you can rename the branch:\n\n```sh\ngit branch -m master main\n```\n\nCreate an **empty** repository on GitHub. To ensure the repository is empty you shouldn't add a README file, .gitignore file, or a license yet. For the examples below the GitHub repository will be named `sethmlarson/package-name` but you should substitute that with the GitHub repository name you chose.\n\nWe need to tell our git repository about our new GitHub repository:\n\n```sh\ngit remote add origin ssh://git@github.com/sethmlarson/package-name\n```\n\nChange all the names and URLs be for your own package. Places to update include:\n\n- `README.md`\n- `pyproject.toml` (`project.name` and `project.urls.Home`)\n- `src/{{secure_package_template}}`\n- `tests/test_{{secure_package_template}}.py`\n\nYou should also change the license to the one you want to use for the package. Update the value in here:\n\n- `LICENSE`\n- `README.md`\n\nNow we can create our initial commit:\n\n```sh\ngit add .\n\ngit commit -m \"Initial commit\"\n```\n\nVerify that this commit is signed. If not you should configure git to auto-sign commits:\n\n```sh\n$ git verify-commit HEAD\ngpg: Signature made Fri 15 Jul 2022 10:55:10 AM CDT\ngpg:                using RSA key 9B2E1343B0B201B8883C79E3A99A0A21AD478212\ngpg: Good signature from \"Seth Michael Larson \u003csethmichaellarson@gmail.com\u003e\" [ultimate]\n```\n\nNow we push our commit and branch:\n\n```sh\n$ git push origin main\n\nEnumerating objects: 25, done.\nCounting objects: 100% (25/25), done.\nDelta compression using up to 12 threads\nCompressing objects: 100% (21/21), done.\nWriting objects: 100% (25/25), 17.92 KiB | 1.28 MiB/s, done.\nTotal 25 (delta 0), reused 0 (delta 0), pack-reused 0\nTo ssh://github.com/sethmlarson/package-name\n * [new branch]      main -\u003e main\n```\n\nSuccess! You should now see the commit and all files on your GitHub repository.\n\n## Configuring the GitHub repository\n\n### Dependabot\n\n[Dependabot](https://docs.github.com/en/code-security/dependabot) is a service provided by GitHub that keeps your dependencies up-to-date automatically by creating\npull requests updating individually dependencies on your behalf. Unfortunately, when using Dependabot with any non-trivial number\nof dependencies the number of pull requests quickly becomes too much to handle, especially\nwhen you think about a single maintainer needing to manage multiple\nprojects worth of dependency updates.\n\nThe approach taken with Dependabot in this repository is to keep the number of pull requests from\nDependabot to a minimum while still maintaining a secure and maintained set of\ndependencies for developing and publishing packages. The policy is described below:\n\n- Always create pull requests upgrading dependencies if the pinned version has a public vulnerability.\n  **This is the default behavior of Dependabot and can't be disabled.**\n- Create pull requests when new major versions of development dependencies are made available.\n  This is important because usually major versions contain backwards-incompatible changes so\n  may actually require changes on our part.\n- Create pull requests when there's a new version of a dependency that carries security sensitive data like\n  `certifi`. It's always important to have this package be up-to-date to avoid monster-in-the-middle (MITM) attacks.\n- All other upgrades to dependencies need to be done manually. These are cases like bug fixes that\n  are impacting the project or new features. The developer experience here is the same\n  as if Dependabot wasn't automatically upgrading dependencies.\n\nYou can [read the `dependabot.yml` configuration file](https://github.com/sethmlarson/secure-python-package-template/blob/main/.github/dependabot.yml) to learn how to\nencode the above policy or [read the Dependabot documentation](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file) on the configuration format.\n\n#### Enabling Dependabot\n\n- Settings \u003e Code security and analysis\n- Dependency graph should be enabled. This is the default for public repos.\n- Enable Dependabot security updates\n\n#### Upgrading dependencies manually\n\nAny upgrades to development dependencies to fix bugs or use new features\nwill require a manual upgrade instead of relying on Dependabot to keep things up to date\nautomatically. This can be done by running the following to upgrade only one package:\n\n```shell\n# We want to only upgrade the 'keyring' package\n# so we use the --upgrade-package option.\npip-compile \\\n  requirements/publish.in \\\n  -o requirements/publish.txt \\\n  --no-header \\\n  --no-annotate \\\n  --generate-hashes \\\n  --upgrade-package=keyring\n```\n\n### CodeQL and vulnerable code scanning\n\n- CodeQL is already configured in `.github/workflows/codeql-analysis.yml`\n- Configure as desired after reading the [documentation for CodeQL](https://codeql.github.com/docs).\n\n### Protected branches\n\n- Settings \u003e Branches\n- Select the \"Add rule\" button\n- Branch name pattern should be your default branch, usually `main`\n- Enable \"Require a pull request before merging\"\n  - Enable \"Require approvals\". To get a perfect score from OpenSSF scorecard metric \"[Branch Protection](https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection)\" you must set the number of required reviewers to 2 or more.\n  - Enable \"Dismiss stale pull request approvals when new commits are pushed\"\n  - Enable \"Require review from Code Owners\"\n- Enable \"Require status checks to pass before merging\"\n  - Add all status checks that should be required. For this template they will be:\n    - `Analyze (python)`\n    - `Test (3.8)`\n    - `Test (3.9)`\n    - `Test (3.10)`\n  - Ensure the \"source\" of all status checks makes sense and isn't set to \"Any source\".\n    By default this should be configured properly to \"GitHub Actions\" for all the above status checks.\n  - Enable \"Require branches to be up to date before merging\". **Warning: This will increase the difficulty to receive contributions from new contributors.**\n- Enable \"Require signed commits\". **Warning: This will increase the difficulty to receive contributions from new contributors.**\n- Enable \"Require linear history\"\n- Enable \"Include administrators\". This setting is more a reminder and doesn't prevent administrators from temporarily disabling this setting in order to merge a stuck PR in a pinch.\n- Ensure that \"Allow force pushes\" is disabled.\n- Ensure that \"Allow deletions\" is disabled.\n- Select the \"Create\" button.\n\n### Protected tags\n\n- Settings \u003e Tags \u003e New rule\n- Use a pattern of `*` to protect all tags\n- Select \"Add rule\"\n\n### Publish GitHub Environment\n\n- Settings \u003e Environments \u003e New Environment\n- Name the environment: `publish`\n- Add required reviewers, should be maintainers\n- Select \"Save protection rules\" button\n- Select \"Protected Branches\" in the deployment branches dropdown\n- Select \"Add secret\" in the environment secrets section\n- Add the PyPI API token value under `PYPI_TOKEN`\n\n### Private vulnerability reporting\n\n- Settings \u003e Code security and analysis\n- Select \"Enable\" for \"Private vulnerability reporting\". This will allow\n  users to privately submit vulnerability reports directly to the repository.\n- Update the URL in the `SECURITY.md` file to the URL of your own repository.\n\n### Secret scanning\n\n- Settings \u003e Code security and analysis\n- Select \"Enable\" for \"Secret scanning\". This will scan and report\n  published tokens to their respective services (like AWS, GCP, GitHub Tokens, etc)\n  so they can be revoked before they're used by a malicious party.\n- Also enable \"Push Protection\" which scans incoming commits for secrets before they\n  are made publicly available. This should provide even more protection from accidentally\n  publishing secrets to a git repository.\n\n## Configuring PyPI\n\nPyPI is increasing the minimum requirements for account security and credential management to make consuming packages on PyPI more secure. This includes [eventually requiring 2FA for all users and requiring API tokens to publish packages](https://pyfound.blogspot.com/2020/01/start-using-2fa-and-api-tokens-on-pypi.html). Instead of waiting for these best practices to become required we can opt-in to them now.\n\n### Opt-in to required 2FA\n\nIf you don't have 2FA enabled on PyPI already there's a section in the [PyPI Help page](https://pypi.org/help) about how to enable 2FA for your account. To make 2FA required for the new project:\n\n- Open \"Your projects\" on PyPI\n- Select \"Manage\" for the project\n- Settings \u003e Enable 2FA requirement for project\n\n### Configuring a Trusted Publisher\n\nIf your project is hosted on GitHub you can take advantage of a new PyPI feature called \"[Trusted Publishers](https://docs.pypi.org/trusted-publishers/)\".\nIt's recommended to use a Trusted Publisher over an API key or password because it provides an additional layer of security\nby requiring the package to originate from a pre-configured GitHub repository, workflow, and environment.\n\nThere's a [short guide on how to add a Trusted Publisher to the project](https://docs.pypi.org/trusted-publishers/adding-a-publisher/).\nBelow is an example of how to map the publishing GitHub Workflow definition to the PyPI Trusted Publisher.\n\n\u003e **Warning**\n\u003e Care should be taken that the publishing workflow can only be triggered\n\u003e by the GitHub accounts that you intend. Remember that git tags (without Protected Tags enabled)\n\u003e only require write access to the repository. This is why GitHub Environments with\n\u003e a set of required reviewers is highly recommended to have an explicit list of\n\u003e people who are allowed to completely execute the publish job.\n\nConfiguring the Trusted Publisher requires 4 values:\n\n- GitHub repository owner\n- GitHub repository name\n- GitHub workflow filename\n- GitHub environment name (optional, but highly recommended!)\n\nUsing this repository ([https://github.com/sethmlarson/secure-python-package-template](https://github.com/sethmlarson/secure-python-package-template)) as an example, the values to set up a Trusted Publisher would be:\n\n- GitHub repository owner: `sethmlarson`\n- GitHub repository name: `secure-python-package-template`\n- GitHub workflow filename: `publish.yml`\n- GitHub environment name: `publish`\n\nBelow is the minimum configurations required from the GitHub Workflow:\n\n```yaml\n# Filename: '.github/workflows/publish.yml'\n# Note that the 'publish.yml' filename doesn't need the '.github/workflows' prefix.\njobs:\n  publish:\n    # ...\n    permissions:\n      # This permission allows for the gh-action-pypi-publish\n      # step to access GitHub OpenID Connect tokens.\n      id-token: write\n\n    # This job requires the 'publish' GitHub Environment to run.\n    # This value is also set in the Trusted Publisher.\n    environment:\n      name: \"publish\"\n\n    steps:\n    # - ...\n    # The 'pypa/gh-action-pypi-publish' action reads OpenID Connect\n    # Note that there's zero config below, it's all magically handled!\n    - uses: \"pypa/gh-action-pypi-publish@0bf742be3ebe032c25dd15117957dc15d0cfc38d\"\n```\n\n## Verifying configurations\n\n### Verifying reproducible builds\n\nFind the latest release that was done via the publish GitHub Environment, I used [v0.1.0](https://github.com/sethmlarson/python-package-template/runs/7163956796?check_suite_focus=true)\nfor this example.\n\nOpen the [corresponding release page on PyPI](https://pypi.org/project/secure-package-template/0.1.0).\nSelect the \"[Download files](https://pypi.org/project/secure-package-template/0.1.0/#files)\" tab.\nFor each `.whl` file select \"view hashes\" and copy the SHA256 and save the value somewhere (`de58d65d34fe9548b14b82976b033b50e55840324053b5501073cb98155fc8af`)\n\nClone the GitHub repository locally. Don't use an existing clone of the repository to avoid tainting the workspace:\n\n```sh\ngit clone ssh://git@github.com/sethmlarson/secure-python-package-template\n```\n\nCheck out the corresponding git tag.\n\n```sh\ngit checkout v0.1.0\n```\n\nRun below command and export the stored value into `SOURCE_DATE_EPOCH`:\n\n```sh\n$ git log -1 --pretty=%ct\n1656789393\n\n$ export SOURCE_DATE_EPOCH=1656789393\n```\n\nInstall the dependencies for publishing and build the package:\n\n```sh\npython -m pip install -r requirements/publish.txt\npython -m build\n```\n\nCompare SHA256 hashes with the values on PyPI, they should match the SHA256 values that we saw on PyPI earlier.\n\n```sh\n$ sha256sum dist/*.whl\nde58d65d34fe9548b14b82976b033b50e55840324053b5501073cb98155fc8af\n```\n\n## License\n\nCC0-1.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsethmlarson%2Fsecure-python-package-template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsethmlarson%2Fsecure-python-package-template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsethmlarson%2Fsecure-python-package-template/lists"}