Skip to main content

Security

Security is mostly a platform promise, not a checklist copied into every repository.

Platform sideDeveloper side
Workload hardening, tenant isolation, network policy, and control-plane RBACChoose an approved template and declare only app-specific access
Token issuance, expiry, revocation, and audit retentionKeep scoped tokens in the CI secret store, never in Git
Git-provider credentials and their encryption keyRefer to a connection ID; never handle the provider credential
Signing policy, trusted keys, and admission policySign uploads when policy requires it and fix rejected artifacts
Authentication middleware and security profilesSelect the approved profile or authentication mode

Workload hardening (always on)

Every rendered pod, without opt-in:

  • non-root, read-only root filesystem, all capabilities dropped, seccomp RuntimeDefault, no service-account token;
  • namespaces labeled for Pod Security Admission restricted;
  • a default-deny NetworkPolicy admitting only the ingress-controller namespace and same-namespace peers — selecting the application label so canary and blue/green tracks are covered too;
  • builds run rootless (BuildKit without privilege, no Docker socket), with path-traversal- and symlink-rejecting artifact extraction.

Tenant isolation and control-plane RBAC

One namespace per tenant per environment (pgf-<tenant>-<env>). API tokens map to principals and every read and write is tenant-scoped; the tenant label travels on all resources. Built-in or custom roles are bound at tenant, application, repository, environment, or infrastructure scope. A token may narrow its principal's bindings but cannot grant permissions the caller does not possess.

New tokens are random opaque credentials. Only their SHA-256 hashes are stored; expiry and revocation are checked on every request. Legacy PAGIFIER_API_TOKENS entries remain bootstrap administrators during migration.

Git provider credentials are a separate secret class. They are encrypted with AES-256-GCM using resource-bound associated data, so ciphertext cannot be moved between tenants or connections. The Helm chart creates and retains the encryption key unless an operator supplies an existing Secret. Every state-changing API call lands in the audit table.

The built-in roles cover the usual jobs:

RoleIntended use
adminTenant bootstrap and control-plane administration
maintainerManage repositories and pipelines, then request deployments
builderRead repositories and run builds
deployerPromote artifacts into allowed environments and infrastructure
approverApprove gated deployments without gaining build or admin access
viewerRead-only application, pipeline, artifact, repository, and infrastructure access

Bind roles as narrowly as practical: an application, repository, application/environment, infrastructure target, or the whole tenant. See Git integration for a token example.

Security header profiles

security: strict in a project expands to a header bundle from the config bucket:

# security/strict.yaml
headers:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; base-uri 'self'

Profiles ship for basic, recommended (the platform default), strict, internal, and pci. Header values are escaped when rendered into nginx configuration, so a header value can never inject server directives.

Edge authentication

authentication: oidc | jwt | oauth2-proxy is enforced at the ingress via platform-owned annotation sets (middleware/authentication-<type>.yaml). Two properties are guaranteed:

  • a missing middleware definition fails the upload with the exact object key to fix — never a silently unprotected app;
  • middleware annotations override project annotations, so an app cannot annotate its way around authentication.

authentication: basic is enforced inside nginx with an htpasswd secret the team controls.

Supply chain

Artifact signing (upload side). A tenant policy at security/signing/<tenant>.yaml can require uploads to carry a cosign sign-blob signature over app.zip:

required: true
publicKey: |
-----BEGIN PUBLIC KEY-----
...

Unsigned or wrongly-signed uploads are rejected with 403. The artifact digest is recorded at upload and re-verified inside the build job before anything runs.

Image signing and provenance (push side). With the platform signing secret configured (signingSecret chart value), every built image is signed with cosign and carries a SLSA provenance attestation recording the artifact digest, build commands, base images, and timestamps:

cosign verify --key cosign.pub registry/acme/frontend:b-abc123…
cosign verify-attestation --key cosign.pub --type slsaprovenance registry/acme/frontend:b-abc123…

Both steps are hard failures — a signing tenant can never receive an unsigned image. Admission controllers (Kyverno, sigstore policy-controller) can then require Pagifier's signature cluster-wide.