Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
6acbe04
Add credential vault, external stores, and automation API
Jun 3, 2026
e39bce1
Complete vault plan: docs, tests, and webhook retries
Jun 3, 2026
d66b7d6
Finish plan follow-ups: i18n, Snyk fixes, and Vault dev
Jun 3, 2026
3a6b7a6
Translate vault and automation UI strings for all locales
Jun 3, 2026
16636b3
Polish vault and automation translations across 21 locales
Jun 3, 2026
3bbb790
Add Docker build scripts and GHA publish workflow.
Jun 3, 2026
fee99af
Fix Infisical action reference to general-alexson/.github-private.
Jun 3, 2026
5567b06
Vendor Infisical OIDC action locally for Docker workflow.
Jun 3, 2026
d567bcc
Use org-shared general-alexson/.github-private Infisical action again.
Jun 3, 2026
940b2d2
Vendor Infisical action: public repo cannot use private org actions.
Jun 3, 2026
c23067c
Use general-alexson/.github-private Infisical action for private repo.
Jun 3, 2026
ab7f2a9
Vendor Infisical OIDC action for public fork.
Jun 3, 2026
cd35306
Run Docker workflow on self-hosted gen runners for Infisical.
Jun 3, 2026
a663577
Fix CI on rootless Podman gen runners: Node frontend and podman build.
Jun 3, 2026
6c2a175
Use Docker Buildx on gen runners; build frontend with Node.
Jun 3, 2026
8097c00
Require Node.js > 20 for frontend CI builds.
Jun 3, 2026
f087b3b
Enable Yarn via Corepack before install; fix setup-node cache error.
Jun 3, 2026
22dfafb
Fix frontend TypeScript errors for vault UI components.
Jun 3, 2026
1cd0750
Build image with host docker build; drop Buildx on gen runners.
Jun 3, 2026
bd6ef7b
Use podman --remote for image build on gen runners.
Jun 3, 2026
538a98f
Harden frontend yarn install on self-hosted runners (cache, retries, …
Jun 3, 2026
4558d01
Fix CI: enable Yarn before caching; setup-node cache requires yarn on…
Jun 3, 2026
2b7f0ca
Use fully qualified Docker Hub names for Podman short-name policy.
Jun 3, 2026
476b65e
Add Ansible and GHA workflow to deploy NPM on oci-test with Docker an…
Jun 4, 2026
a0d1c12
Build Docker image on push only when Dockerfile changes; always tag l…
Jun 4, 2026
039ed6c
Fix deploy Ansible: inventory file, oci_test limit, Ansible 9+, no po…
Jun 4, 2026
a5f3d46
Fix deploy workflow Python path for gen runner ci-venv.
Jun 4, 2026
332797c
corrected hostname in inventory
Jun 4, 2026
8eeebeb
Use Ansible SSH key pair instead of CA-signed certificates.
Jun 4, 2026
b94cadc
Fix deploy SSH host key verification on self-hosted runners.
Jun 4, 2026
65fce25
Fix CI SSH host key checks and pin ansible-core 2.17.
Jun 4, 2026
7e55c26
Fix Ansible version check and upgrade runner 2.10 to 2.17.
Jun 4, 2026
dcc3bd4
Fix ansible 2.17 version detection after pip upgrade.
Jun 4, 2026
18d3924
Fix NPM startup on IPv6-less hosts and setup.js promise chain.
Jun 4, 2026
20945b0
Add semantic versioning (vX.Y.Z) for code and Docker Hub images.
Jun 4, 2026
1f6360e
Overhaul external credential stores and refresh DNS credential list.
Jun 4, 2026
55c0125
Fix Biome a11y and hook dependency lint errors in frontend.
Jun 4, 2026
445dba3
Fix Infisical secret resolve to use secretPath and secret key.
Jun 4, 2026
be240e0
Publish vX, vX.Y, and vX.Y.Z Docker tags from VERSION on branch builds.
Jun 4, 2026
37aba12
Bundle VitePress docs in admin UI and simplify Infisical auth.
Jun 4, 2026
9636d34
Fix VitePress dead links to API reference index.
Jun 4, 2026
5cc37de
Remove unused editingId from buildApiPayload.
Jun 4, 2026
73f7a53
Pin @ungap/structured-clone to 1.3.1 in docs dependencies.
Jun 4, 2026
d625742
Add OpenAPI operation descriptions, docs proxy example, and dev Swagg…
Jun 4, 2026
a56515e
Remove duplicate sidebar on the in-app documentation page.
Jun 4, 2026
f1b728a
Move DNS credentials into Settings as a tab.
Jun 4, 2026
ed0959e
Migrate frontend, backend, test, and docs tooling from Yarn to npm.
Jun 4, 2026
526e9ed
Merge upstream/develop into develop (sync fork)
Jun 4, 2026
1e9f1a4
Harden security, document policy, and refresh docs.
Jun 4, 2026
ab6cccb
Fix Infisical OIDC JWT path check for self-hosted runners.
Jun 4, 2026
5a3e14c
Use upstream .version file instead of VERSION.
Jun 4, 2026
cfa57a2
Leave versioning to upstream; fork images use branch tags only.
Jun 4, 2026
ab95aa7
Remove GitHub Actions references from documentation.
Jun 4, 2026
07a7128
Remove fork image section from README.
Jun 4, 2026
bb639a5
Make documentation read as upstream; remove fork references.
Jun 4, 2026
c8f7121
Add rehype-sanitize to frontend dependencies for HelpModal build.
Jun 4, 2026
56152b9
Fix VitePress dead link to nginx docs-api-proxy example.
Jun 4, 2026
2ef618f
Configure Snyk ignores for express-fileupload and Infisical OIDC script.
Jun 4, 2026
235b771
updated .snyk
Jun 4, 2026
546866e
feat: settings hub, automation API, bundled docs, npm migration
Jun 4, 2026
3d3225b
fix(ci): build docs in isolated temp dir to avoid Jenkins race
Jun 4, 2026
2e32649
fix(test): align health schema and VaultIntegration with Cypress CI
Jun 4, 2026
e84b5cb
fix(ci): use npm install for isolated docs build in frontend-build
Jun 4, 2026
def4977
fix(ci): point docs openapi generation at mounted repo root
Jun 4, 2026
a20688e
fix(test): use cy.env array API for VaultIntegration skip
Jun 4, 2026
c6fa7c6
fix(schema): restore automation API OpenAPI paths and response schemas
Jun 4, 2026
8a7a519
fix(test): correct CredentialProviders and Jobs Cypress assertions
Jun 4, 2026
8e15128
fix(cypress): treat null job error field as success in API client
Jun 4, 2026
0073d75
fix(frontend): decouple Utils tests from compiled locale JSON
Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
15 changes: 15 additions & 0 deletions .snyk
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Snyk policy — accepted risks, false positives, and paths excluded from Snyk Code.
version: v1.25.1
ignore:
SNYK-JS-EXPRESSFILEUPLOAD-2635697:
- "*":
reason: No patched express-fileupload release; upload hardened in backend/app.js (limits, safeFileNames).
created: 2026-06-03T00:00:00.000Z
expires: 2027-06-03T00:00:00.000Z
SNYK-JS-EXPRESSFILEUPLOAD-2635946:
- "*":
reason: No patched express-fileupload release; upload hardened in backend/app.js (limits, safeFileNames).
created: 2026-06-03T00:00:00.000Z
expires: 2027-06-03T00:00:00.000Z
exclude: {}
patch: {}
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ so that the barrier to entry here is low.
- Access Lists and basic HTTP Authentication for your hosts
- Advanced Nginx configuration available for super users
- User management, permissions, and audit log
- **Credential vault** on the `/data` volume (encrypted DNS API tokens, optional external stores via OIDC)
- **Automation API** (API keys, async certificate jobs, signed webhooks) — see [docs](docs/src/advanced/automation-api.md)
- **Settings** UI for DNS credentials, external credential stores, API keys, and webhooks (see [SECURITY.md](SECURITY.md) for reporting vulnerabilities)

::: warning
`armv7` is no longer supported in version 2.14+. This is due to Nodejs dropping support for armhf. Please
Expand Down Expand Up @@ -74,6 +77,8 @@ services:

This is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more.

**Important:** Mount `./data:/data` so the credential vault, encryption keys, and automation settings persist across container restarts. Optional: set `NPM_SECRETS_ENCRYPTION_KEY` (32+ bytes, base64) to control encryption instead of the auto-generated key under `/data/keys/`.

3. Bring up your stack by running

```bash
Expand Down
48 changes: 46 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

## Supported Versions

Only the latest stable release receives security updates.
Older versions are not actively maintained.
Only the latest stable release receives security updates. Older versions are not actively maintained.

| Version | Supported |
| ------- | --------- |
Expand All @@ -19,12 +18,57 @@ See all releases: https://github.com/NginxProxyManager/nginx-proxy-manager/relea
**Do NOT open a public GitHub Issue to report a security vulnerability.**

Use GitHub's private vulnerability reporting:

https://github.com/NginxProxyManager/nginx-proxy-manager/security/advisories/new

Please include:

- Affected version (Docker image tag or release)
- Description of the vulnerability
- Steps to reproduce
- Potential impact

Once a fix is available, a public GitHub Security Advisory will be published.

## Dependency and code scanning

Maintainers may use [Snyk](https://snyk.io/) for SCA and SAST. Policy exceptions and documented false positives live in [`.snyk`](.snyk).

Local checks (from repo root):

```bash
cd frontend && npm ci && npm audit
cd ../backend && npm ci && npm audit
cd ../test && npm ci && npm audit
cd ../docs && npm ci && npm audit
```

## Known accepted risks

| Item | Mitigation |
|------|------------|
| `express-fileupload@1.5.2` (no patched release) | Upload middleware limited in [`backend/app.js`](backend/app.js): `limits.fileSize`, `abortOnLimit`, `safeFileNames`, `preserveExtension`. Documented in `.snyk`. |
| Bundled docs iframe (`/documentation`) | Only allowlisted `?section=` keys map to VitePress paths under `/docs/`; iframe uses `sandbox`. |

Revisit `.snyk` entries when dependencies ship fixes or when mitigations change.

## Deployment and secrets hygiene

- Mount **`/data`** persistently so encryption keys and the credential vault survive restarts.
- Prefer **`NPM_SECRETS_ENCRYPTION_KEY`** (32-byte value, base64) in production instead of the auto-generated key under `/data/keys/`.
- Do not commit SSH keys, vault tokens, or API keys in the repository.
- Restrict admin port **81** to trusted networks; use strong passwords, 2FA, and scoped **API keys** for automation.
- Automation tokens (`npmak_…`) are shown once at creation; treat them like passwords.

## Secure development

- Package management uses **npm** and `package-lock.json`.
- JSON API routes return structured JSON via `res.json()` to avoid accidental HTML reflection.
- Production error responses do not include stack traces unless debug mode is enabled.
- OpenAPI operation descriptions are maintained in [`backend/schema/scripts/operation-descriptions.json`](backend/schema/scripts/operation-descriptions.json); regenerate the docs OpenAPI bundle after schema changes ([`docs/README.md`](docs/README.md)).

## Related documentation

- [Automation API](docs/src/advanced/automation-api.md) — API keys, webhooks, credential vault
- [Advanced configuration](docs/src/advanced-config/index.md) — `/data` volume and encryption
- In-app help: **Settings** → DNS credentials, external stores, API keys, webhooks (`/settings?tab=…`; `/credentials` redirects to DNS credentials)
13 changes: 10 additions & 3 deletions backend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ import mainRoutes from "./routes/main.js";
* App
*/
const app = express();
app.use(fileUpload());
app.use(
fileUpload({
limits: { fileSize: 50 * 1024 * 1024 },
abortOnLimit: true,
safeFileNames: true,
preserveExtension: true,
}),
);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

Expand Down Expand Up @@ -71,7 +78,7 @@ app.use((err, req, res, _) => {
payload.error.message_i18n = err.message_i18n;
}

if (isDebugMode() || (req.baseUrl + req.path).includes("nginx/certificates")) {
if (isDebugMode()) {
payload.debug = {
stack: typeof err.stack !== "undefined" && err.stack ? err.stack.split("\n") : null,
previous: err.previous,
Expand All @@ -86,7 +93,7 @@ app.use((err, req, res, _) => {
}
}

res.status(err.status || 500).send(payload);
res.status(err.status || 500).json(payload);
});

export default app;
120 changes: 120 additions & 0 deletions backend/internal/api-key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import apiKeyModel from "../models/api_key.js";
import now from "../models/now_helper.js";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import internalAuditLog from "./audit-log.js";

const omissions = () => ["is_deleted", "key_hash", "key_prefix"];

const hashApiKey = (rawKey) => bcrypt.hash(rawKey, 13);
const verifyApiKey = (rawKey, hash) => bcrypt.compare(rawKey, hash);

const internalApiKey = {
create: async (access, data) => {
await access.can("api_keys:create", data);

const prefix = crypto.randomBytes(4).toString("hex");
const secret = crypto.randomBytes(24).toString("base64url");
const rawKey = `npmak_${prefix}_${secret}`;
const keyHash = await hashApiKey(rawKey);

const row = await apiKeyModel.query().insertAndFetch({
name: data.name,
key_prefix: prefix,
key_hash: keyHash,
owner_user_id: access.token.getUserId(1),
permissions: data.permissions || {},
expires_on: data.expires_on || null,
});

const result = utils.omitRow(omissions())(row);
result.key = rawKey;

await internalAuditLog.add(access, {
action: "created",
object_type: "api-key",
object_id: row.id,
meta: { name: row.name },
});

return result;
},

getAll: async (access) => {
await access.can("api_keys:list");
return apiKeyModel
.query()
.where("is_deleted", 0)
.orderBy("name", "ASC")
.then(utils.omitRows(omissions()));
},

delete: async (access, data) => {
await access.can("api_keys:delete", data.id);
const row = await apiKeyModel
.query()
.where("id", data.id)
.andWhere("is_deleted", 0)
.first();

if (!row) {
throw new errs.ItemNotFoundError(data.id);
}

await apiKeyModel.query().patchAndFetchById(row.id, { is_revoked: 1, is_deleted: 1 });

await internalAuditLog.add(access, {
action: "deleted",
object_type: "api-key",
object_id: row.id,
meta: { name: row.name },
});

return true;
},

/**
* @param {string} rawKey
*/
authenticate: async (rawKey) => {
if (!rawKey?.startsWith("npmak_")) {
throw new errs.AuthError("Invalid API key");
}

const parts = rawKey.split("_");
if (parts.length < 3) {
throw new errs.AuthError("Invalid API key");
}

const prefix = parts[1];
const row = await apiKeyModel
.query()
.where("key_prefix", prefix)
.andWhere("is_deleted", 0)
.andWhere("is_revoked", 0)
.first();

if (!row) {
throw new errs.AuthError("Invalid API key");
}

if (row.expires_on && new Date(row.expires_on) < new Date()) {
throw new errs.AuthError("API key expired");
}

const valid = await verifyApiKey(rawKey, row.key_hash);
if (!valid) {
throw new errs.AuthError("Invalid API key");
}

await apiKeyModel.query().patchAndFetchById(row.id, {
last_used_at: now(),
});

return row;
},
};

export default internalApiKey;
3 changes: 2 additions & 1 deletion backend/internal/audit-log.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import errs from "../lib/error.js";
import { scrubAuditMeta } from "../lib/secrets/scrub.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import auditLogModel from "../models/audit-log.js";

Expand Down Expand Up @@ -94,7 +95,7 @@ const internalAuditLog = {
action: data.action,
object_type: data.object_type || "",
object_id: data.object_id || 0,
meta: data.meta || {},
meta: scrubAuditMeta(data.meta || {}),
});
},
};
Expand Down
Loading