Engineering · 16 Apr 2026

Hardening a self-hosted payment gateway: a practical deployment guide.

Network, containers, secrets, database, supply chain, and monitoring — the controls that matter when the software that moves value runs on your infrastructure.

Published
16 Apr 2026
Reading time
6 min
Readers
—
Topics
StablePaySecurityDockerPostgreSQLOperations
On this page

Key takeaways

  • Self-hosting makes you responsible for the whole security surface; a layered baseline is affordable and effective.
  • Reduce what is reachable, reduce what a compromise can do, and make abnormal activity loud.
  • Secrets, database access, and the signing path deserve the most care; backups and restores deserve a drill.
  • Security is a routine: patching, rotation, and review on a schedule, with named owners.

When you self-host a payment gateway, security stops being a line in a vendor's trust page and becomes your own engineering. That is the point — you get to inspect and control it — and also the responsibility. The good news is that a hardened deployment is not exotic. It is a layered set of ordinary controls applied consistently.

This guide is written for the engineer who has just been handed a gateway to deploy. It is not a compliance framework, and it does not replace a review by your own security team. It is a practical baseline organised by layer, with the reasoning for each control so you can adapt it.

A mental model: reduce, contain, detect

GoalQuestionExamples
Reduce exposureWhat can an attacker reach?Private networks, minimal open ports, allow-lists
Contain impactIf something is compromised, what can it do?Least privilege, separate credentials, read-only filesystems
Detect and recoverWill we notice, and can we rebuild?Logging, alerts, tested backups, rotation
EdgeTLS, WAF, rate limits
GatewayAPI and dashboard, minimal surface
DataPostgreSQL, private network
SecretsSecret store, narrow access
SigningPolicy-gated, logged
Layers between the internet and the signing key

Layer 1: the network

Network baseline

  • Only the intended endpoints (checkout, API, webhook return path) are reachable from the internet.
  • The database has no public address and accepts connections only from the gateway's network.
  • The dashboard and admin routes are reachable only through a VPN, allow-list, or single sign-on.
  • TLS terminates at a hardened edge with modern protocol settings; no plaintext internal secrets.
  • Egress is restricted where practical: the gateway needs to reach RPC providers and your webhook targets, not the whole internet.
SurfaceRecommended posture
Public APIRate-limited, authenticated, input-validated [4]
CheckoutPublic, but minimal; no secrets, no admin functions
Operator dashboardPrivate; single sign-on with multi-factor authentication
DatabasePrivate network only; strong authentication [5]
RPC providersOutbound only; credentials in the secret store

Layer 2: containers and hosts

A container is not a security boundary by itself, but sensible defaults shrink what a compromise can do. The OWASP Docker guidance [1] covers this in depth; these are the highest-value items.

Illustrative — a minimal, non-root imageDockerfile
FROM node:22-slim AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:22-slim
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /app/.next ./.next
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
USER node                       # never run as root
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD node -e "fetch('http://localhost:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "node_modules/next/dist/bin/next", "start"]
Illustrative — runtime restrictions in a compose fileYAML
services:
  gateway:
    image: registry.example.com/gateway:1.4.2      # pin a version, never :latest
    read_only: true                                # immutable root filesystem
    tmpfs: ["/tmp"]
    user: "node"
    cap_drop: ["ALL"]                              # no extra Linux capabilities
    security_opt: ["no-new-privileges:true"]
    environment:
      DATABASE_URL_FILE: /run/secrets/database_url # secrets from files, not env dumps
    secrets: [database_url]
    networks: [internal]
    deploy:
      resources:
        limits: { cpus: "1.0", memory: 512M }
  • Pin image versions and verify digests. A moving tag means you deploy different code without deciding to.
  • Scan images. Run a vulnerability scanner in the pipeline and fail on serious findings with a fixable version.
  • Keep hosts minimal and patched. Fewer packages, automatic security updates, and an owner for the base image.
  • Limit resources. A runaway process should hurt itself, not its neighbours.

Layer 3: secrets

Secrets are the crown jewels: database credentials, webhook secrets, RPC keys, and above all anything involved in signing. The OWASP secrets-management guidance [2] and the twelve-factor principle of configuration in the environment [7] agree on the essentials, refined by modern practice: prefer short-lived, narrowly scoped credentials pulled at runtime over long-lived values baked into images or files.

PracticeWhyHow
No secrets in repositories or imagesThey persist in history and layersSecret store; scan commits for secrets
Load at runtimeKeeps them out of build artefactsMount from the secret manager
Least privilegeLimits the blast radiusOne credential per service, per environment
Rotate on a schedule and on eventsShrinks the window of a leakAutomate; drill the procedure [8]
Separate environmentsA staging leak must not matterDifferent secrets and stores per environment
Audit accessYou must see who read whatLog secret reads; alert on anomalies

Layer 4: the database

Payment state lives in PostgreSQL. It deserves authentication, least privilege, encryption, and above all, backups you have actually restored.

Illustrative — least-privilege rolesSQL
-- the application role can read and write its own tables, nothing else
CREATE ROLE gateway_app LOGIN PASSWORD :'app_password';
GRANT CONNECT ON DATABASE gateway TO gateway_app;
GRANT USAGE ON SCHEMA public TO gateway_app;
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO gateway_app;
REVOKE DELETE, TRUNCATE ON payments, payment_events FROM gateway_app;  -- history is append-only

-- a separate role runs migrations; the app role cannot alter schema
CREATE ROLE gateway_migrator LOGIN PASSWORD :'migrator_password';
GRANT ALL ON SCHEMA public TO gateway_migrator;
ControlDetail
AuthenticationStrong, certificate- or scram-based authentication; restrict by network [5]
Encryption in transitRequire TLS for all connections
Encryption at restEncrypted volumes and encrypted backups
Append-only historyRevoke delete on audit and event tables from the app role
Connection limitsPrevent connection exhaustion

Backups and restores

A backup you have never restored is a hope. Continuous archiving and point-in-time recovery [6] let you rebuild to a chosen moment, which matters when the problem is a bad change rather than a dead disk.

  1. 01

    Take base backups on a schedule

    Plus continuous archiving of write-ahead logs.

  2. 02

    Encrypt and store them elsewhere

    A different account or region, with separate credentials.

  3. 03

    Restore into a scratch environment monthly

    Time it. Verify the data and run reconciliation against it.

  4. 04

    Write the recovery procedure down

    Including who decides and how long recovery should take.

  5. 05

    Alert on backup failures

    A silent backup failure is discovered at the worst time.

Layer 5: the application

  • Validate all input. Treat every request field as untrusted; the API security top ten [4] is a good checklist for object-level authorisation, rate limiting, and mass assignment.
  • Verify webhook signatures on the raw body. And enforce timestamp tolerance.
  • Authenticate operators properly. Single sign-on, multi-factor authentication, and role-based access to dangerous actions.
  • Log security-relevant events. Logins, permission changes, signature requests, and replays, to storage the application cannot alter.
  • Handle errors safely. No stack traces or secrets in responses.

The signing path is special

Anything that can request a signature deserves extra controls: a narrow interface, policy checks, approval requirements, and alerts on unusual use. See the signing-boundary post for the design.

Layer 6: supply chain and change

RiskControl
Vulnerable dependencyAutomated dependency updates and scanning; a patch cadence
Malicious or compromised packageLockfiles, pinned versions, review of significant updates
Tampered imageSigned images and digest pinning; a trusted registry
Unreviewed change to productionReviewed pull requests, protected branches, a deployment audit trail
Stale vendor softwareSubscribe to the vendor's release notes; apply patch releases promptly

Layer 7: monitoring and alerting

Hardening reduces the chance of an incident. Monitoring decides how long one lasts. Alert on things that should be rare, not on everything.

SignalWhy it matters
Signing requests outside policy or hoursPossible misuse or compromise
New admin users or role changesPrivilege escalation
Repeated authentication failuresCredential stuffing or probing
Unexpected outbound connectionsExfiltration or malware
Indexer lag, webhook failures, unmatched paymentsOperational health that also signals attack
Backup and certificate expiryPreventable outages

A hardening review you can run this week

Quarterly review

  • Confirm what is exposed to the internet, and remove anything unintended.
  • Verify the database is not publicly reachable and roles are least-privilege.
  • Rotate one secret end to end, following the runbook.
  • Restore last night's backup into a scratch environment.
  • Check image and dependency scan results; apply pending patch releases.
  • Review who has access to production, the secret store, and signing.
  • Test one alert by causing the condition on purpose.

Application security verification standards [3] give a more formal way to structure such a review as your programme matures. Start with the baseline above, own it with names and dates, and improve it deliberately. A boring, consistently applied deployment is far safer than an ambitious one nobody maintains.

References & further reading

  1. 1
    Docker Security Cheat Sheet — OWASP Cheat Sheet Series
  2. 2
    Secrets Management Cheat Sheet — OWASP Cheat Sheet Series
  3. 3
  4. 4
  5. 5
    PostgreSQL Documentation: Client Authentication — PostgreSQL Global Development Group
  6. 6
  7. 7
    The Twelve-Factor App — Adam Wiggins, 12factor.net
  8. 8
Found this useful? Share it

Get the next essay in your inbox

Practical writing on payments infrastructure, operations software, and shipping real systems. No spam, no sales sequence.

We only use your email to send the studio's writing. See the privacy policy.

About the author

Product behaviour described here reflects what is implemented and tested; anything else is marked as planned. Code samples are illustrative.

All writing

Have a system like this to run?

Explore the catalog, or write down the problem and the constraints. We respond when the fit is real.

Free apps from the studio. Enter your email, get a private download link. Free for personal use.

Get them free

Have a product to sell? We review, list, and sell it for you — you keep 90% of every sale.

Apply to sell with us