Chapter 12
Best Practices
⏱ 15 minutes · A checklist for shipping protected Python, in the order things usually go wrong. Each item says where this repository already does it.
Secrets
- No secrets in code, images, Dockerfiles or Git. This repository has none:
.envis ignored,.env.exampleholds only harmless defaults. - Use a secrets manager (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, HashiCorp Vault, Doppler, or Docker/Kubernetes secrets) and inject secrets at runtime.
- Prefer short-lived, narrowly scoped credentials, and rotate them. A leaked token that expires in an hour is a much smaller problem.
- Environment variables are acceptable for configuration, not ideal for secrets: they show
up in
docker inspect,/proc, crash reports and child processes. Mounted secret files (/run/secrets/...) are a step better.
Least privilege and container hardening
- Run as a non-root user. ✓
USER app(uid 10001) in theDockerfile. - Code is read-only to the app. ✓ root-owned files, plus
read_only: truein compose. - Drop capabilities, forbid privilege escalation. ✓
cap_drop: [ALL],no-new-privileges:true. - Publish only what is needed. ✓
127.0.0.1:8300locally; no published port behind Caddy (deploy/). - Small images. ✓
python:3.12-slim, multi-stage, no build tools in the final image. - Pin the base image by digest for reproducible builds:
FROM python:3.12-slim@sha256:...(theDockerfileexplains how).
Filesystem permissions
- Application code: owned by root, readable by the app user, writable by nobody at runtime.
- Writable paths: only what the app needs (
/tmpas tmpfs here), never the code folder. - Secret files: mode
0400, readable only by the app user.
Dependencies
- Pin exact versions. ✓
pyproject.tomlpins direct dependencies;uv.lockandrequirements.txtpin everything. - Verify hashes. ✓
pip install --require-hashesin theDockerfile. - Scan for known vulnerabilities, in CI and on a schedule:
uvx pip-audit==2.10.0 -r requirements.txtWhat it does: checks every pinned package against the Python Packaging Advisory Database.
Expected: No known vulnerabilities found, or a list of packages and fixed versions.
- Update deliberately: one dependency per change, with tests (the
dependency-updateskill describes a safe routine). Remember to regeneraterequirements.txt:uv export --frozen --no-default-groups --no-emit-project --format requirements-txt -o requirements.txt.
Image scanning and SBOM
- Scan the image, not just your Python packages: the base image has system packages too.
✓ CI scans with Trivy (see
.github/workflows/ci.yml). - Produce an SBOM (software bill of materials), so you can answer "are we affected?" the day a new vulnerability is announced. ✓ CI generates an SPDX SBOM with Syft and keeps it as a build artifact.
Locally, with Docker:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:0.74.0 image --ignore-unfixed --severity HIGH,CRITICAL pyarmor-demo:localWhat it does: scans the image's OS packages (Debian) and Python packages for known HIGH and
CRITICAL vulnerabilities that have a fix available (--ignore-unfixed).
Why: a finding with a fix is something you can act on: rebuild on a newer
python:3.12-slim, or bump the package.
Expected: a summary table with 0 for every Python package and for the Debian base, at
the time of writing. Drop --ignore-unfixed to see everything: in our run, the Python packages
still had 0, and the Debian base had several dozen HIGH findings that Debian had not fixed yet
(status affected). Track those; they disappear as the base image is updated.
CI/CD protection
- Build protected artifacts in CI, not on a laptop, so every release is reproducible and reviewed. ✓ CI obfuscates, verifies and smoke-tests on every push.
- PyArmor licenses are CI secrets. Never commit a registration file. If you use a paid
license in CI, store the regfile as an encrypted GitHub secret, write it to a temporary file in
the job, and run
pyarmor regthere. Note that PyArmor's CI license sends some runner details to its license server, and has rate limits. Read the license page. - Least-privilege CI tokens. ✓
permissions: contents: readin the workflow. - Pin third-party actions to a release (better: a commit SHA), and review updates.
- Protect the main branch: required reviews and required checks.
Private package repositories and artifact integrity
- Publish protected images to a private registry (GHCR, ECR, Artifact Registry) with access control, never to a public one if the logic matters.
- If you distribute wheels, use a private package index (an Artifactory, Nexus or cloud equivalent) rather than a public PyPI project.
- Publish checksums (
sha256sum) for every downloadable artifact.
Signed releases
- Sign container images and verify signatures before deployment, for example with
Sigstore cosign:
cosign signin CI,cosign verifyat deploy. - Sign executables (Windows Authenticode, Apple notarisation) for PyInstaller bundles.
- Sign Git tags for releases.
Server access controls
- SSH keys only, no passwords; restricted to known addresses or a VPN or bastion.
- Separate accounts, no shared root; every privileged action is logged.
- A firewall that allows only the ports you serve (here: 80, 443).
- Automatic security updates for the host OS.
- Monitoring and alerting on health checks and unusual traffic.
Where PyArmor fits
| Control | Protects against |
|---|---|
| PyArmor | casual reading and copying of the code you ship (scenario B) |
| Secrets manager | leaked credentials |
| Access control, network policy | strangers reaching the environment (scenario C) |
| Container hardening | a compromised process doing more damage |
| Dependency and image scanning | known vulnerabilities in what you did not write |
| Signed artifacts, protected CI | tampered or unofficial builds |
Use PyArmor when you ship code to environments you do not control and the logic is worth protecting. Use all the rest regardless.
That is the workshop. Applying it to code you own? See Protect your own project.
You protected an application, proved it still works, shipped it in a container with no source, and saw exactly where obfuscation stops. Questions or improvements are welcome: see Contributing in the README.