Package: secure-package-template
Secure Python package templateTemplate for a Python package with a secure
project host and package repository configuration.The goals of this project are to:Show how to configure a Python package hosted on GitHub with:Operational security best-practicesAutomated publishing to PyPICode quality and vulnerability scanningBuild reproducibilityReleases with provenance attestationObtain a perfect rating fromOpenSSF ScorecardSLSA Level 3using GitHub OIDCConfiguring git for commit and tag signingInfoCommit and tag signing is a practice that's recommended to avoid commit author spoofing
but isn't strictly required for a secure project configuration.
If you'd like to skip this step, you can jump ahead tocreating a GitHub repository.Git needs to be configured to be able to sign commits and tags. Git uses GPG for signing, so you need tocreate a GPG keyif you don't have one already. Make sure you use aemail address associated with your GitHub accountas the email address for the key. If you wish to keep your email address private you should use GitHub's providednoreplyemail address.gpg--full-generate-keyAfter you've generated a GPG key you need toadd the GPG key to your GitHub account.
Then locally you canconfigure git to use your signing key:gitconfig--global--unsetgpg.formatList GPG secret keys, in this example the key ID is '3AA5C34371567BD2'$gpg--list-secret-keys--keyid-format=long
/Users/hubot/.gnupg/secring.gpg
------------------------------------
sec4096R/3AA5C34371567BD22016-03-10[expires:2017-03-10]uidHubot<hubot@example.com>
ssb4096R/4BB6D45482678BE32016-03-10Tell git about your signing key:gitconfig--globaluser.signingkey3AA5C34371567BD2Then tell git to auto-sign commits and tags:gitconfig--globalcommit.gpgsigntruegitconfig--globaltag.gpgSigntrueNow all commits and tags you create from this git instances will be signed and show up as "verified" on GitHub.Creating the GitHub repositoryClone this repository locally:gitclonessh://git@github.com/sethmlarson/secure-python-package-templateRename the folder to the name of the package and remove existing git repository:mvsecure-python-package-templatepackage-namecdpackage-name
rm-rf.gitCreate a new git repository and ensure the branch name ismain:$gitinit
InitializedemptyGitrepositoryin.../package-name/.git/

$gitstatus
Onbranchmain

Nocommitsyet
...If the branch isn't namedmainyou can rename the branch:gitbranch-mmastermainCreate anemptyrepository 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 namedsethmlarson/package-namebut you should substitute that with the GitHub repository name you chose.We need to tell our git repository about our new GitHub repository:gitremoteaddoriginssh://git@github.com/sethmlarson/package-nameChange all the names and URLs be for your own package. Places to update include:README.mdpyproject.toml(project.nameandproject.urls.Home)src/{{secure_package_template}}tests/test_{{secure_package_template}}.pyYou should also change the license to the one you want to use for the package. Update the value in here:LICENSEREADME.mdNow we can create our initial commit:gitadd.

gitcommit-m"Initial commit"Verify that this commit is signed. If not you should configure git to auto-sign commits:$gitverify-commitHEAD
gpg:SignaturemadeFri15Jul202210:55:10AMCDT
gpg:usingRSAkey9B2E1343B0B201B8883C79E3A99A0A21AD478212
gpg:Goodsignaturefrom"Seth Michael Larson <sethmichaellarson@gmail.com>"[ultimate]Now we push our commit and branch:$gitpushoriginmain

Enumeratingobjects:25,done.
Countingobjects:100%(25/25),done.
Deltacompressionusingupto12threads
Compressingobjects:100%(21/21),done.
Writingobjects:100%(25/25),17.92KiB|1.28MiB/s,done.
Total25(delta0),reused0(delta0),pack-reused0Tossh://github.com/sethmlarson/package-name*[newbranch]main->mainSuccess! You should now see the commit and all files on your GitHub repository.Configuring the GitHub repositoryDependabotDependabotis a service provided by GitHub that keeps your dependencies up-to-date automatically by creating
pull requests updating individually dependencies on your behalf. Unfortunately, when using Dependabot with any non-trivial number
of dependencies the number of pull requests quickly becomes too much to handle, especially
when you think about a single maintainer needing to manage multiple
projects worth of dependency updates.The approach taken with Dependabot in this repository is to keep the number of pull requests from
Dependabot to a minimum while still maintaining a secure and maintained set of
dependencies for developing and publishing packages. The policy is described below:Always create pull requests upgrading dependencies if the pinned version has a public vulnerability.This is the default behavior of Dependabot and can't be disabled.Create pull requests when new major versions of development dependencies are made available.
This is important because usually major versions contain backwards-incompatible changes so
may actually require changes on our part.Create pull requests when there's a new version of a dependency that carries security sensitive data likecertifi. It's always important to have this package be up-to-date to avoid monster-in-the-middle (MITM) attacks.All other upgrades to dependencies need to be done manually. These are cases like bug fixes that
are impacting the project or new features. The developer experience here is the same
as if Dependabot wasn't automatically upgrading dependencies.You canread thedependabot.ymlconfiguration fileto learn how to
encode the above policy orread the Dependabot documentationon the configuration format.Enabling DependabotSettings > Code security and analysisDependency graph should be enabled. This is the default for public repos.Enable Dependabot security updatesUpgrading dependencies manuallyAny upgrades to development dependencies to fix bugs or use new features
will require a manual upgrade instead of relying on Dependabot to keep things up to date
automatically. This can be done by running the following to upgrade only one package:# We want to only upgrade the 'keyring' package# so we use the --upgrade-package option.pip-compile\requirements/publish.in\-orequirements/publish.txt\--no-header\--no-annotate\--generate-hashes\--upgrade-package=keyringCodeQL and vulnerable code scanningCodeQL is already configured in.github/workflows/codeql-analysis.ymlConfigure as desired after reading thedocumentation for CodeQL.Protected branchesSettings > BranchesSelect the "Add rule" buttonBranch name pattern should be your default branch, usuallymainEnable "Require a pull request before merging"Enable "Require approvals". To get a perfect score from OpenSSF scorecard metric "Branch Protection" you must set the number of required reviewers to 2 or more.Enable "Dismiss stale pull request approvals when new commits are pushed"Enable "Require review from Code Owners"Enable "Require status checks to pass before merging"Add all status checks that should be required. For this template they will be:Analyze (python)Test (3.8)Test (3.9)Test (3.10)Ensure the "source" of all status checks makes sense and isn't set to "Any source".
By default this should be configured properly to "GitHub Actions" for all the above status checks.Enable "Require branches to be up to date before merging".Warning: This will increase the difficulty to receive contributions from new contributors.Enable "Require signed commits".Warning: This will increase the difficulty to receive contributions from new contributors.Enable "Require linear history"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.Ensure that "Allow force pushes" is disabled.Ensure that "Allow deletions" is disabled.Select the "Create" button.Protected tagsSettings > Tags > New ruleUse a pattern of*to protect all tagsSelect "Add rule"Publish GitHub EnvironmentSettings > Environments > New EnvironmentName the environment:publishAdd required reviewers, should be maintainersSelect "Save protection rules" buttonSelect "Protected Branches" in the deployment branches dropdownSelect "Add secret" in the environment secrets sectionAdd the PyPI API token value underPYPI_TOKENPrivate vulnerability reportingSettings > Code security and analysisSelect "Enable" for "Private vulnerability reporting". This will allow
users to privately submit vulnerability reports directly to the repository.Update the URL in theSECURITY.mdfile to the URL of your own repository.Configuring PyPIPyPI is increasing the minimum requirements for account security and credential management to make consuming packages on PyPI more secure. This includeseventually requiring 2FA for all users and requiring API tokens to publish packages. Instead of waiting for these best practices to become required we can opt-in to them now.Opt-in to required 2FAIf you don't have 2FA enabled on PyPI already there's a section in thePyPI Help pageabout how to enable 2FA for your account. To make 2FA required for the new project:Open "Your projects" on PyPISelect "Manage" for the projectSettings > Enable 2FA requirement for projectConfiguring a Trusted PublisherIf your project is hosted on GitHub you can take advantage of a new PyPI feature called "Trusted Publishers".
It's recommended to use a Trusted Publisher over an API key or password because it provides an additional layer of security
by requiring the package to originate from a pre-configured GitHub repository, workflow, and environment.There's ashort guide on how to add a Trusted Publisher to the project.
Below is an example of how to map the publishing GitHub Workflow definition to the PyPI Trusted Publisher.WarningCare should be taken that the publishing workflow can only be triggered
by the GitHub accounts that you intend. Remember that git tags (without Protected Tags enabled)
only require write access to the repository. This is why GitHub Environments with
a set of required reviewers is highly recommended to have an explicit list of
people who are allowed to completely execute the publish job.Configuring the Trusted Publisher requires 4 values:GitHub repository ownerGitHub repository nameGitHub workflow filenameGitHub environment name (optional, but highly recommended!)Using this repository (https://github.com/sethmlarson/secure-python-package-template) as an example, the values to set up a Trusted Publisher would be:GitHub repository owner:sethmlarsonGitHub repository name:secure-python-package-templateGitHub workflow filename:publish.ymlGitHub environment name:publishBelow is the minimum configurations required from the GitHub Workflow:# Filename: '.github/workflows/publish.yml'# Note that the 'publish.yml' filename doesn't need the '.github/workflows' prefix.jobs:publish:# ...permissions:# This permission allows for the gh-action-pypi-publish# step to access GitHub OpenID Connect tokens.id-token:write# This job requires the 'publish' GitHub Environment to run.# This value is also set in the Trusted Publisher.environment:name:"publish"steps:# - ...# The 'pypa/gh-action-pypi-publish' action reads OpenID Connect# Note that there's zero config below, it's all magically handled!-uses:"pypa/gh-action-pypi-publish@0bf742be3ebe032c25dd15117957dc15d0cfc38d"Verifying configurationsVerifying reproducible buildsFind the latest release that was done via the publish GitHub Environment, I usedv0.1.0for this example.Open thecorresponding release page on PyPI.
Select the "Download files" tab.
For each.whlfile select "view hashes" and copy the SHA256 and save the value somewhere (de58d65d34fe9548b14b82976b033b50e55840324053b5501073cb98155fc8af)Clone the GitHub repository locally. Don't use an existing clone of the repository to avoid tainting the workspace:gitclonessh://git@github.com/sethmlarson/secure-python-package-templateCheck out the corresponding git tag.gitcheckoutv0.1.0Run below command and export the stored value intoSOURCE_DATE_EPOCH:$gitlog-1--pretty=%ct1656789393$exportSOURCE_DATE_EPOCH=1656789393Install the dependencies for publishing and build the package:python-mpipinstall-rrequirements/publish.txt
python-mbuildCompare SHA256 hashes with the values on PyPI, they should match the SHA256 values that we saw on PyPI earlier.$sha256sumdist/*.whl
de58d65d34fe9548b14b82976b033b50e55840324053b5501073cb98155fc8afLicenseCC0-1.0