Docker: docker pull jeffcaldwellca/mkcertweb:5.0.0 · ghcr.io/jeffcaldwellca/mkcertweb:v5.0.0 — upgrading from 4.4.x? Read Upgrading from 4.4.x below first.
Highlights
- Split-pane dashboard. Every page has a top bar (page tabs, mkcert / CA /
openssl status, user, theme) with a list pane on the left and a detail pane
on the right. The certificates page groups certificates as needs attention,
healthy, read-only and archived, has a quick-generate command bar and a
filter, and shows overview, files and notes tabs for the selected
certificate. The selection is kept in the URL. - Root CA and notifications pages. The root CA (details, download, install
guide, generate) and the notification channels (email, ntfy, webhook,
monitoring, with their test actions and the expiring list) moved off the
certificates page onto their own pages. - Settings page with a section list, a setting search, the config key on
every field, unsaved-change tracking (editedsections,discard, a
leave-page warning) and the running configuration. - SCEP page rebuilt: service URLs with copy buttons, enterprise CA status,
templates, and challenges / issued / manual enroll / mdm setup tabs. A new
challenge password is shown once. - Light and dark themes on every page, including login, and a layout that
works down to phone width.
Upgrading from 4.4.x
- Docker: the image's
HEALTHCHECKand the compose health check now
probe/api/healthinstead of/; change any custom health check the same
way. SetTRUST_PROXYto your reverse proxy's address as the container sees
it (see docs/DOCKER.md). -
docker run: add-v mkcert_config:/app/configso settings saved in
the UI (config/settings.json) survive recreating the container, as
docker-compose.ymlandnpm run docker:runnow do. - systemd: the unit moved to
deploy/mkcert-web-ui.serviceand keeps
the CA in/var/lib/mkcert-web-ui/ca. Before starting it, copy the existing
rootCA.pemandrootCA-key.pemthere (or mkcert creates a new CA), then
create the directories it writes to:
cd /opt/mkcert-web-ui && sudo install -d -o www-data -g www-data certificates config data ssl.
See deploy/README.md. -
ENABLE_OIDC=truewithoutENABLE_AUTH=true: the settings API and
mkcert -uninstall(POST /api/uninstall-ca, anduninstall-cain
POST /api/execute) now return 403. SetENABLE_AUTH=true; without it
there was never a login, OIDC or otherwise. -
FORCE_HTTPS=truenow redirects pages, assets and API calls: 301 for
GET and HEAD, 308 for other methods. Only/api/healthand/scepare
exempt. Scripts that call the API overhttp://must switch tohttps://. - API clients:
- The removed endpoints and response fields listed under Breaking are gone;
use the replacements given there. - Archive and restore return 409
TWIN_EXISTSinstead of overwriting a
same-name copy; restore takes?swap=trueto exchange the two. ?archivedand?swapaccept onlytrueorfalse(anything else is a 400).DELETE /api/certificates/:folder/:certnameremoves one copy only (see
Breaking for which one).GET /apikeys are now"METHOD /path".- Every JSON 500 body is a fixed
errormessage plus aref(and a short
reasonon a few routes). Don't parse the error text; look up theref
in the server log. - Status codes: archive when the active files are gone → 404 (was 200);
restore when the archived files are gone → 404 (was 200); an unexpected
upload failure → 500 (was 400); a malformed JSON body → 400 (was 500); an
oversized body → 413 (was 500).
- The removed endpoints and response fields listed under Breaking are gone;
- UI: uploaded and SCEP-issued certificates are view-only. The root-ca
page's install button really runsmkcert -installon the server (it needs
root, so it fails in Docker and under the systemd unit; install the CA on
clients instead). An archived certificate with an active copy of the same
name shows their shared note read-only; edit it on the active copy (see
Known issues).
Breaking
- Removed endpoints (they now return 404):
GET /api/files,
GET /api/file/:filename/content,GET /download/:filename,
GET /api/certificate/:filename,DELETE /api/certificate/:filenameand
GET /api/commands. Use/api/download/{cert,key,bundle}/:folder/…and
/api/certificates/:folder/:certname. GET /apiindex format.endpoints.<group>keys are now
"METHOD /path"with a description as the value (was"/path": "METHOD - …",
which also lost one of two routes sharing a path). The index lists every API
route.- Same-name archived and active certificates are never overwritten:
DELETE /api/certificates/:folder/:certnamedeletes exactly one copy:
?archived=truethe archived one,?archived=falsethe active one, no
parameter the active one if present, else the archived one. It used to
delete both. The response addsscope.POST …/restorereturns 409TWIN_EXISTSwhen an active copy exists,
instead of overwriting it;?swap=trueexchanges the two copies.POST …/archivereturns 409TWIN_EXISTSwhen an archived copy exists,
instead of overwriting it.
?archivedand?swapaccept onlytrueorfalse; anything else is a 400.- A certificate name that decodes to a path (e.g. a double-encoded
archive%252F<name>) is rejected with 400. - Uploaded and SCEP-issued certificates are view-only in the UI (no download,
PFX, archive or notes buttons); the server's per-certificate routes never
accepted their folders. - Removed response fields that exposed server paths and that the UI
never read:commandandcertificateDirfromPOST /api/execute
(generate); the absolutepathof each file inGET /api/certificates
(relativePathstays);workingDirectoryfromGET /api/system;
publicCACertPathfromPOST /api/generate-ca; and from the public
GET /api/status:caRoot,ca.root,server.pid,server.memory,
server.versionandenvironment.workingDir(features.oidcis now a
boolean). - systemd unit moved to
deploy/mkcert-web-ui.serviceand now keeps the
root CA in/var/lib/mkcert-web-ui/ca. When upgrading, copyrootCA.pem
androotCA-key.pemfrom the service user's old mkcert directory there
first, or mkcert creates a new CA (seedeploy/README.md). FORCE_HTTPS=trueredirects everything over plain HTTP (pages, assets,
API calls), with 308 for methods other than GET and HEAD, except
/api/healthand/scep. It used to reach only unknown paths, and it
trustedX-Forwarded-Protofrom any client; now only fromTRUST_PROXY
(see Fixed).- The settings API and uninstall-ca return 403 unless
ENABLE_AUTH=true,
even withENABLE_OIDC=true(see Security). - Health check path: the Docker and compose health checks probe
/api/health, not/. - Archive returns 404 when the active files are gone (it returned 200), and
restore returns 404 when the archived files are gone (it returned 200 and
moved nothing). - Error bodies and status codes: every JSON 500 is a fixed message plus a
ref(no error text, and no stack withNODE_ENV=development); unexpected
upload failures are 500 (were 400); malformed JSON is 400 and an oversized
body 413 (both were 500); a broken multipart upload is 400Malformed upload.
See Security and the upgrade checklist above.
Added
TRUST_PROXYenvironment variable: which proxies may set
X-Forwarded-For/X-Forwarded-Proto(true,false, a hop count, or
addresses, CIDRs andloopback/linklocal/uniquelocal). The default is
the previous hard-codedloopback,linklocal,uniquelocal, so existing
proxy-container setups keep working; that default trusts every private-LAN
client, so set it to your proxy's address as the app sees it (in Docker,
usually the network gateway or the proxy container's IP; see
docs/DOCKER.md).
Changed
- Accessibility: focus returns to a sensible place after every re-render;
status dots and channel states carry text, not only colour; toasts are
announced through live regions (errors stay until dismissed); sub-tabs work
with the arrow keys. - Content-Security-Policy:
script-srcandstyle-srcare'self'only, with
nounsafe-inlineand no CDN origin. No page has inline scripts, styles or
event handlers, and guard tests keep it that way. - Font Awesome is gone; icons are text glyphs. No page loads anything from
another origin. - The root-ca page's install button calls
POST /api/install-ca, which always
runsmkcert -install. The old path reported success without installing
when the CA files already existed. - Least-privilege systemd unit: the code tree is read-only to the service,
writes are limited tocertificates/,config/,data/andssl/, and the
CA lives in the unit's state directory. - Uploads are staged in a private directory under the OS temp dir instead of
the working directory. data/(certificate notes) is git-ignored.- The unused
asn1jsdependency is removed. - Pages link the favicons and web manifest;
/favicon.icoredirects to
/assets/favicon.ico. - CI runs
npm ciandnpm teston Node.js 22 (theenginesminimum) for
pull requests and pushes tomain; the Docker scan and publish jobs wait
for it. package.json: theenvscript is gone (server.jsloads.envitself),
docker:runmounts themkcert_configvolume like docker-compose, the
compose:*scripts usedocker compose, and thelicense(GPL-3.0-only)
andrepositoryfields are set.docker-compose.ymlno longer setsHOST=0.0.0.0, which had no effect.- The top bar's CA dot says
presentrather thaninstalled: it shows that
the CA files exist in CAROOT, not that the CA is in a trust store. - The Docker
HEALTHCHECKand the docker-compose healthcheck probe
/api/healthinstead of/(it needs no login and is never redirected by
FORCE_HTTPS). Custom health checks that probe/should switch too. - Documentation rewritten and checked against the code: new
docs/API.md,
CONTRIBUTING.mdanddeploy/README.md;.env.examplelists every
variable, commented out.
Fixed
- Data loss with same-name certificates: deleting an archived certificate also
deleted the active certificate and key of the same name; restoring
overwrote the active copy; archiving overwrote an older archived copy. POST …/archivereturns 404 when the active files are already gone (it
reported success), so the UI reloads a stale list.- An interrupted swap can't be made worse: restore returns 409
SWAP_INCOMPLETEwhile a.swap-*leftover exists, failed swaps roll back,
and the listing hides the leftovers. - An upload with an invalid file name no longer leaves the request hanging.
- The certificate list tolerates files or folders deleted during a scan, and
a missingcertificates/folder gives an empty list instead of a 500. - Settings: saving with an invalid field in another section (or hidden by the
search) now shows and focuses that field. - Settings: "copy" in the running-config dialog works on plain HTTP (it
selects the text when the clipboard API is unavailable). - The UI no longer offers actions the server always rejects: buttons for
uploaded and SCEP certificates, and download/bundle/PFX for archived ones. - Command bars and card grids no longer overflow narrow screens.
- The settings footer said saved settings override
.env; it is the other way
round (environment variables that are set win), and now says so. - SCEP page: removed the
ENTERPRISE_CA_ORGANIZATIONand
ENTERPRISE_CA_ORGANIZATIONAL_UNITvariables (nothing reads them) and the
claim that challenges are configured through environment variables; they are
created on the page. Key size and M365 key usage text now match the code. - Webhook and ntfy test hints said "save, then test"; settings apply after a
restart, so they now say to restart first. - The upload dialog and file picker advertised
.cer,.p7band.p7c, which
the server rejects; they now list.pem,.crt,.key,.p12and.pfx. - A failed PFX export with a password no longer returns the openssl command
line and absolute server paths in the 500 body; it returns
failed to generate PFXand logs the details. POST /api/executeno longer has thelistcommand, which always failed
with 500; it now gets the same 400 as any unknown command.FORCE_HTTPS=trueredirected almost nothing: the redirect was registered
after the static files and every route, so only unknown paths reached it.
It now runs first, so pages, assets and API calls over plain HTTP are
redirected to the HTTPS port: 301 for GET and HEAD, 308 for other methods
(a 301 turned a POST into a GET). Only/api/healthand/scepare
exempt, and nothing is redirected until the HTTPS server is listening.
"Already HTTPS" means a TLS connection, orX-Forwarded-Proto: httpsfrom
a trusted proxy (TRUST_PROXY); the header from any other client is
ignored. The startup log mentions the redirect only once HTTPS is
listening, and warns when HTTPS failed to start so redirects are off.- With
ENABLE_AUTH=true, opening/without a session now redirects to
/loginon the server. The static file handler used to answer/with
index.htmlfirst, so the redirect only happened after the page's first API
call. The other pages (/index.htmlincluded) are still static files that
go to/loginwhen their first API call returns 401. - Static files are served from the app's directory, not the working
directory, so startingnode /path/to/server.jsfrom elsewhere serves the UI. - The startup log's "Certificate storage" line names
<working directory>/certificates, the directory the certificate routes
use; it showed the directory next toserver.js. - The root-ca install guide's Linux command copied
rootCA.pem, but the
download is namedmkcert-rootCA.pem; the command and the download button
now use that name. - The SCEP page claimed compatibility with Microsoft Intune, VMware Workspace
ONE, Cisco Meraki and Apple Profile Manager, which was never tested. It now
says it works with standard SCEP clients that use a static challenge
password;docs/SCEP.mdexplains why MDMs such as Intune, which need NDES
or a challenge-validation API, can't use it. - The ntfy test button ("send test notification") failed against every
server since 8f8e972: its title contained an em dash, which Node refuses
in an HTTP header (ERR_INVALID_CHAR). The title is plain ASCII now, and
any non-ASCII title or tags (for example from a certificate name) are sent
RFC 2047-encoded (=?UTF-8?B?…?=), which ntfy decodes. The message body
is sent astext/plain; charset=utf-8. .env.exampleandexamples/*.envhad comments on the same line as
values. dotenv strips them, butdocker run --env-filekeeps them, so
CERT_MONITOR_UPLOADED=false # …was read as true. Comments are now on
their own line.
Security
- 5xx responses no longer leak server internals. Several error bodies
(certificate generation, root CA info/install/uninstall/generate, archive,
restore, delete, downloads, SCEP, notification tests, monitoring, upload)
echoederror.message, mkcert/openssl stderr or the command line, which can
include absolute paths, the CA directory and the home directory, to anyone
who could call the route (everyone whenENABLE_AUTHis off).
NODE_ENV=developmentalso put the raw message and stack in every 500.
Every JSON 500 now returns a fixed message and aref; the full error is
logged under thatref. Notification tests and CA install/uninstall/
generate add a short sanitisedreason(e.g.ECONNREFUSED). Toasts show
the reason and the ref. Unexpected upload failures are now a 500 (they were a 400 with the
fs error), and a file that can't be saved is reported without the fs error.
Malformed requests are 4xx again: bad JSON is400 Invalid request body
(it was a 500), an oversized body is a 413, and a broken multipart upload
is400 Malformed upload; none of them log the raw body. A guard test
keeps error details out of 5xx bodies and every JSON 500 onserverError. GET /api/statusdisclosed server internals without a login. The
route is public (the top bar shows the mkcert / CA / openssl dots before
login), even withENABLE_AUTH=true, and it returned the CA directory, the
working directory, the process ID, memory usage, the Node version and, as
features.oidc, the OIDC client ID. It now returns only the booleans the UI
uses, plus uptime, platform and architecture.ENABLE_OIDC=truewithoutENABLE_AUTH=trueleft the settings API and
mkcert -uninstallopen to anyone. Those guards accepted the OIDC flag as
"authentication enabled", but withoutENABLE_AUTHthere is no login at all
(the OIDC routes aren't mounted and route auth is a no-op), so anyone on the
network could read the masked settings, rewrite credentials or remove the CA
from the trust store. The settings API (403) and both uninstall paths (403)
now requireENABLE_AUTH=true.
Removed
- The old UI files
public/script.js,public/styles.cssand
public/settings.js, and Font Awesome. - The release-note files
docs/RELEASE-v3.1.0.md,RELEASE-v4.0.0.mdand
RELEASE-v4.1.0.md(this changelog has the history),docs/PAGES.md
(folded intoCONTRIBUTING.md) and the internaldocs/superpowers/notes. setup-demo.sh,scripts/check-shared-tokens.sh(a test covers it) and
.github/copilot-instructions.md.- Dead server code:
src/utils/fileValidation.jsand unused helpers.
Known issues
- Exporting a PFX with a password fails with 500; a PFX without a password
works. - Archive and restore move only
<name>.pemand<name>-key.pem, socrtand
p12certificates can't be archived. - SCEP manual enrollment with extra names or a UPN (including every
M365Usercertificate) saves<cn>+<n>.pem, which the issued list doesn't
show. - Downloads and PFX export never look in
archive/; the UI hides those
buttons for archived certificates. - An active certificate and its same-name archived copy share one note: the
server stores notes per folder + name, so editing either changes both. The
UI shows the archived copy's note read-only while the active copy exists
("shares a note with the active …");PUT …/notesthrough the API still
writes the shared note. - helmet's default
upgrade-insecure-requestsCSP directive can leave the page
unstyled when it's opened over plain HTTP from another machine on the LAN.
Use HTTPS or a TLS-terminating proxy. GET /api/scep/enterprise-ca/status(and the SCEP page) always reports the
enterprise CA as disabled, even whenENTERPRISE_CA_*is set and manual
enrollment uses it.- With
CERT_MONITOR_UPLOADED=true(the default) the monitor scans
certificates/uploaded/twice, so an expiring uploaded certificate is
listed and alerted twice. Set it tofalsewhen uploads live under
certificates/. NTFY_PRIORITY(ntfy.priority) is shown on the notifications page but not
used: alerts go out athigh, orurgentwhen a certificate is critical.
HOST,THEME_PRIMARY_COLORandTHEME_DARK_MODEare also read but have no
effect.
