podsquire
podsquire is a small container init wrapper for workloads that need SPIFFE
certificates, local mTLS proxy listeners, Vault-backed configuration, and simple
subprocess supervision without baking that plumbing into the application.
It can run as PID 1 around your application, as a sidecar, or as a one-shot init container that writes certificates to a shared volume.
Features
- SPIFFE/SPIRE certificate bootstrap — fetches an X.509 SVID from the SPIRE
Workload API, writes
tls.crt,tls.key,ca.crt, and optionally a combined key+cert PEM file, then renews before expiry. - Static certificate mode — use existing cert/key/CA files when SPIRE is not available, for example in local development.
- mTLS proxy listeners — expose local plaintext HTTP or TCP listeners that connect to upstream services with the SPIFFE client certificate.
- Subprocess supervision — launch an application command, optionally signal it on certificate or secret refresh, and restart it on failure with bounded retry limits.
- Vault secret injection — authenticate to HashiCorp Vault with the pod's Kubernetes service account token and deliver KV secrets as environment variables or an atomically-written JSON file.
- User-defined proxy presets — define your own environment-specific proxy shortcuts in configuration; no private service definitions are bundled in the package.
- Shared platform service directories — load common proxy presets from a local directory/file or HTTP(S) directory/file at startup, with retries and warn-and-continue defaults.
All features are optional. Enable only the sections your container needs.
Installation
pip install podsquire
For local development from this repository:
pip install -e .
Quick start
Run with a YAML config:
podsquire --config /app/podsquire.yml
Fetch SPIFFE cert material and exit, useful as an init container:
podsquire --pull-certs-only /var/run/secrets/tls
That writes:
/var/run/secrets/tls/tls.crt
/var/run/secrets/tls/tls.key
/var/run/secrets/tls/ca.crt
/var/run/secrets/tls/tls.key+cert
Configuration overview
Copy config-example.yml and adjust it for your environment. Top-level sections
are optional, but a long-running process should include at least one of:
spireorstaticsubprocessproxiesenabled_proxy_presetsenabled_platform_servicesvault_secrets
SPIRE certificate management
spire:
# Optional. Defaults to SPIFFE_ENDPOINT_SOCKET, then /run/spire/sockets/agent.sock.
# socket: unix:///run/spire/sockets/agent.sock
cert_path: /tmp/podsquire/tls.crt
key_path: /tmp/podsquire/tls.key
ca_path: /tmp/podsquire/ca.crt
combined_path: /tmp/podsquire/tls.key+cert
renewal_interval: 60
expiry_threshold: 3600
retry_interval: 5
combined_path writes a single PEM file containing the private key followed by
the certificate chain. Some clients, including PyMongo's
tlsCertificateKeyFile, expect this shape.
Static cert mode
static:
cert_path: /var/run/secrets/tls/tls.crt
key_path: /var/run/secrets/tls/tls.key
ca_path: /var/run/secrets/tls/ca.crt
Static mode does not renew certificates.
Subprocess supervision
subprocess:
command: "python3 -m myapp --port 8080"
path: /app
reload_signal: SIGHUP
restart:
enabled: true
max_restarts: 5
window_seconds: 300
If reload_signal is configured under subprocess, podsquire sends that signal
when SPIFFE certificates are renewed.
Proxy listeners
proxies:
- name: api
mode: http # http or tcp
local_host: 127.0.0.1
local_port: 18080
remote_host: api.default.svc.cluster.local
remote_port: 8443
verify_remote: true
Modes:
| Mode | Behaviour | Use for |
|---|---|---|
http |
HTTP/1.1 reverse proxy; rewrites the Host header |
REST/HTTP APIs |
tcp |
Raw byte tunnel | MongoDB, gRPC, AMQP, custom TCP protocols |
When verify_remote is true, the upstream server certificate is verified
against the SPIFFE trust bundle written to ca_path. Hostname checking is
disabled because SPIFFE SVIDs normally identify workloads with URI SANs rather
than DNS SANs.
Shared platform service directories
Use platform_services when a platform team publishes common proxy listener
definitions outside the application repo. The source can be a local directory, a
local YAML catalogue, an HTTP(S) directory, or an HTTP(S) YAML catalogue. The
path can be set in config or with PODSQUIRE_PLATFORM_SERVICES_PATH.
platform_services:
path: /etc/podsquire/platform-services
retries: 3
retry_interval: 2
timeout: 10
fail_on_load_error: false
fail_on_missing: false
enabled_platform_services:
- vault
- mongo
Directory mode fetches one YAML file per requested service name:
/etc/podsquire/platform-services/vault.yml
/etc/podsquire/platform-services/mongo.yaml
https://platform.example/podsquire/services/vault.yml
Each per-service file may contain a single proxy definition. The name field is
optional in per-service files and defaults to the requested service name:
mode: http
local_host: 127.0.0.1
local_port: 8200
remote_host: vault.example.svc.cluster.local
remote_port: 8200
verify_remote: true
Catalogue mode is selected when the path ends in .yml or .yaml. The file can
contain proxy_presets:, proxies:, or a direct mapping of service name to proxy
definition:
proxy_presets:
vault:
mode: http
local_host: 127.0.0.1
local_port: 8200
remote_host: vault.example.svc.cluster.local
remote_port: 8200
verify_remote: true
Failure behaviour is intentionally non-disruptive by default. If the common
directory/catalogue cannot be loaded, podsquire logs a warning, logs the service
names that could not be loaded, and continues startup with any remaining explicit
proxies: entries. To make missing shared services fatal, set:
platform_services:
fail_on_load_error: true # fatal if the source cannot be loaded
fail_on_missing: true # fatal if a requested service name is absent
Explicit proxies: entries override shared platform services with the same
name, so application-local emergency overrides remain possible.
Proxy presets
Podsquire intentionally does not bundle environment-specific service names or DNS records. If you want short names for your own platform services, define them in your config:
proxy_presets:
vault:
mode: http
local_host: 127.0.0.1
local_port: 8200
remote_host: vault.example.svc.cluster.local
remote_port: 8200
verify_remote: true
mongo:
mode: tcp
local_host: 127.0.0.1
local_port: 27017
remote_host: mongodb.example.svc.cluster.local
remote_port: 27017
verify_remote: true
enabled_proxy_presets:
- vault
- mongo
Explicit entries under proxies: take precedence when they use the same name
as a preset.
Vault secret injection
vault_secrets:
kv_path: apps/my-service/config
url: http://127.0.0.1:8200
role: my-service
kv_mount_point: secret
kv_version: 2
output_mode: env
refresh_interval_minutes: 0
Podsquire authenticates to Vault using the Kubernetes service account token at:
/var/run/secrets/kubernetes.io/serviceaccount/token
Supported output modes:
| Mode | Behaviour | Best for |
|---|---|---|
env |
Inject secrets into podsquire's environment before the subprocess starts/restarts | Apps that read config from env at startup |
json_file |
Write secrets atomically to a JSON file | Apps that can reload config without restart |
env_file |
Write shell-sourceable export KEY=value commands atomically |
CI wrappers that need to source Vault-backed variables |
For shell env-file mode:
vault_secrets:
kv_path: apps/my-service/config
output_mode: env_file
env_file_path: /tmp/env.sh
You can also use env-file mode directly from the CLI for compatibility with legacy CI wrappers:
podsquire --write-env-to-file /tmp/env.sh
source /tmp/env.sh
For JSON-file mode:
vault_secrets:
kv_path: apps/my-service/config
output_mode: json_file
json_file_path: /var/run/secrets/podsquire/vault-secrets.json
reload_signal: SIGUSR1
Environment fallback variables:
| YAML key | Env var | Default |
|---|---|---|
kv_path |
VAULT_KV_PATH |
required if vault_secrets is enabled |
url |
VAULT_URL |
http://127.0.0.1:8200 |
role |
VAULT_ROLE |
podsquire |
kv_mount_point |
VAULT_KV_MOUNT_POINT |
unset |
kv_version |
VAULT_KV_VERSION |
1 |
json_file_path |
VAULT_JSON_FILE_PATH |
required for json_file mode |
env_file_path |
VAULT_ENV_FILE_PATH |
required for env_file mode |
Secret values are never logged. In env mode, all keys returned by Vault are
placed into os.environ for the subprocess to inherit. In env_file mode, values
are safely shell-quoted before being written. Keys containing ToBase64 are
base64-encoded for compatibility with legacy env conventions.
Deployment patterns
PID 1 wrapper
container start → podsquire → fetch certs → start app → supervise app
exec podsquire --config /app/podsquire.yml
Cert init container
podsquire --pull-certs-only /var/run/secrets/tls
Mount the same volume into the main container and point your application at the written PEM files.
Sidecar proxy
Run podsquire with spire/static and proxies, but without subprocess. The
main application container connects to the local service address exposed by the
sidecar, for example 127.0.0.1:27017 for a MongoDB TCP tunnel.
Demo connectivity app
python -m podsquire.connectivity_test is a tiny demo subprocess. Configure its
checks with PODSQUIRE_CHECKS_JSON:
export PODSQUIRE_CHECKS_JSON='[{"name":"vault","url":"http://127.0.0.1:8200/v1/sys/health","ok_statuses":[200,429]}]'
It also demonstrates reloading the JSON secrets file on SIGUSR1.
Release and PyPI publishing
Publishing is automated from GitHub Actions when a SemVer git tag or GitHub
release is created. The package version is derived from git tags with
setuptools-scm; there is no committed version file to update.
Release checklist:
# 1. Make sure the release commit is clean and all checks pass:
python -m compileall podsquire
python -m pytest -q
python -m build
python -m twine check dist/*
# 2. Tag the release commit. The tag is the package version source of truth:
git tag -a v0.1.1 -m "Release v0.1.1"
git push origin main --tags
The publish workflow checks out the tag with full git history, derives the
package version from the tag, and refuses to publish if the derived version does
not exactly match the SemVer tag without its leading v.
The Publish to PyPI workflow uses PyPI Trusted Publishing/OIDC, so no PyPI API
token is required in GitHub secrets. Configure the PyPI project publisher for:
- owner/repository:
tkdpython/podsquire - workflow file:
.github/workflows/publish.yml - environment:
pypi
The CI workflow builds and validates distributions on pushes, pull requests, and manual dispatches, but it does not publish.
Development checks
From the package directory:
python -m compileall podsquire
python -m pytest
python -m build
python -m twine check dist/*
Metadata
Release files for podsquire 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| podsquire-0.1.1.tar.gz | 33.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| podsquire-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 61.7 kB
Release files / podsquire-0.1.1.tar.gz
| Download URL | podsquire-0.1.1.tar.gz |
|---|---|
| Size | 33.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a2e4e82b53e3d83884275109ba225d872b7532eefeeab4ca350f58a0a1324f9c
|
|
BLAKE2b-256 checksum How to use checksums |
2ee83fbf2374d952776740184b7b783a4ae0bfc4c8c089cda9e49e2d1bc7d819
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 12, 2026.
Transparency logRelease files / podsquire-0.1.1-py3-none-any.whl
| Download URL | podsquire-0.1.1-py3-none-any.whl |
|---|---|
| Size | 28.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6ac9f9a5fa0373589ebd6778b4589b2cde084766f60ad433be869dafaba2e2f6
|
|
BLAKE2b-256 checksum How to use checksums |
e9e4cb8cce5e5b2040c766abdf904c87b54b40cc92d44c949e9cfdc73db3af12
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 12, 2026.
Transparency log