Release notes
This is CHANGELOG.md of the repository. The upgrade notes of each release are summarized on
the Upgrade page.
All notable changes to MyOwnPKI are listed here. The format follows Keep a Changelog, and the project uses Semantic Versioning. Before 1.0, a minor version may change the API, the configuration and the database schema; the release notes say how to upgrade.
Unreleased
0.3.8 - 2026-11-13
Charts, reports and analysis: a Reports page with the expiry of the current week and the next 12 weeks, the issuances by source, the certificates per CA, the alert history and the agent fleet, two of the charts on the dashboard, and an Analysis page that applies ten fixed rules and says what each finding saw, why it matters and what to do. The charts are drawn by the server as SVG; no script runs in the browser. Both pages and two new REST endpoints compute everything from data the server already holds. The live test by the maintainer is pending.
Added
- Reports page (
GET /reports, menu Overview): five sections, each a fold with a chart, a sentence that says what is counted and a Numbers table with the same figures. Expiry: 13 weekly columns, the current ISO week and 12 more (UTC, Monday start), colored by the warning and critical windows of the monitor (monitor.expiry_days). Issuances: the last 12 weeks, stacked by source (ACME, agent, UI and API), derived from the ACME orders; imported and external certificates and agent identity certificates are not counted. Authorities: certificates per CA by valid, expiring, expired and revoked, with the certificates that have no CA of this server as External certificates. Alerts: opened per week over five weeks (the 30 days the server keeps) by severity, and how many are open. Agents: state, version against the server and deployments, only while the agent listener is on. Expiry and Authorities are open, the rest closed; the state is not remembered. The numbers are built at most once a minute per tenant and the page says when. - Two charts on the dashboard (Expiry and Authorities) below the table of certificates that expire in 30 days, each in an open fold, with a link to the Reports page. A failed build costs the charts, not the dashboard.
- Analysis page (
GET /analysis,?rule=<id>): a summary line, the findings (Severity, Rule, Subject with a link, What was seen, Why it matters, What to do), the state of every rule (ok,findingsorunknownwith the reason) and a short explanation. Ten rules:orphaned_certificate,duplicate_certificates,renewal_loop,expiry_wave,weak_key,weak_signature,ca_cliff,outdated_agent,unused_acme_accountandunused_domain; the thresholds are in the operations guide §15.2. A rule without the data to judge saysunknownand never produces a finding. The analysis is advice only: it raises no alert, sends no message, stores nothing and has no dismiss. - REST:
GET /api/v1/reportsandGET /api/v1/analysis(with?rule=<id>; an unknown id is400), described inapi/openapi.yaml(tagmonitoring, role viewer,403for a role or token limited to one CA,Cache-Control: no-store, work while the server is sealed). - Migration
pki.db0020: the indexescertificates_created_atand the partialacme_orders_certificate, for the issuance chart. Indexes only. - Chart and fold components (
components.Chart,components.Fold,internal/web/charts): server-rendered SVG with a title, a description, a legend and a numbers table under every chart; the series differ by pattern as well as by color; new tokens--chart-s1to--chart-s3and--chart-w; entries in the design gallery. Tests allow exactly one<svg>shape and check the contrast of the chart colors in the light, dark and OS-follow themes. - UI inventory:
task screenshots -- -inventoryshoots every page of the real UI, light and dark, into the untracked.tmp/ui-inventory/(with the HTML of each page andmanifest.txt), on a demo instance with extra seeds (agents, alerts, endpoints, back-dated issuances). A test fails when a page route is neither in the list nor on the exclusion list.docs/UI-GAPS.mdis the ranked list of what the walk found missing or awkward; nothing in it is built. - Documentation: the site page Reports and analysis, a new operations guide chapter 15, entries in the tour, FAQ, glossary and the lifetimes page.
Changed
- ACME new names page: the text now says what a client does. The first order for a new name is refused with unauthorized … is a new name … waits for approval, the client does not wait, and it has to be run again after the name is approved.
- Screenshots: the certificate page (it shows Revoke now and Plan revocation) and the new names page are refreshed in the README, the tour and the guide.
- Documentation:
docs/PLAN.mdrecords that the charts are server-rendered SVG instead of the ECharts island (see Known limitations). api/openapi.yamlinfo.versionis 0.3.8.
Security notes
- Permission before data. The Reports and Analysis pages, the dashboard charts and both endpoints need the
tenant-wide read permission (
pki.read), like the Alerts page, because they name the CAs, hosts, accounts, agents and domains of the whole tenant. A role or an API token limited to one CA gets403and no menu entry. The check is the first thing the services do; the cache has no key that comes from a request and belongs to the tenant. - Names from outside are escaped. CA, domain, agent and certificate names come from CSRs, ACME clients and agents. The pages escape them in the template, the findings clip them to 120 characters and carry them as plain text in JSON (no field holds HTML), chart texts are stripped of control and bidi characters and clipped, and a finding’s link must be a path inside the UI.
- SVG rules. The only
<svg>the UI may contain has one fixed shape; script,<foreignObject>,<image>,<use>,<style>, links, animation,href,styleand event attributes are refused inside it, and tests render every page against that rule. - The Content Security Policy is unchanged (
script-src 'self'; style-src 'self'); no script was added and the charts need none. No new dependency:go.mod,go.sumandTHIRD_PARTY.mdare unchanged. - Read-only, nothing leaves the server. The pages and endpoints are
GETand change nothing. The two new packages import no network package (a test checks it) and the analysis only reads the stored result of the last TLS scan. - Migration 0020 changes no data.
Known limitations
- The live test by the maintainer is pending. The thresholds of the ten rules are defaults chosen without long
experience, are constants in the program rather than settings, and are to be judged on a real installation; a finding
that cries wolf is a bug report. Expect
orphaned_certificateto sayunknownwithout a TLS scan. - Charts are server-rendered SVG, not ECharts. The plan named an ECharts island. ECharts’ HTML tooltip is reported to
write inline
styleattributes, which the strict CSP refuses (not verified for the current version), the repository has no JavaScript pipeline and esbuild is not an approved dependency. ECharts stays approved and unused; ADR-04 is unchanged. The price: no hover effects, zoom or legend toggles (a bar has the browser’s own tooltip). - The alert history is the 30 days the server keeps (five weeks). There is no dismiss or acknowledge of findings, no export (CSV or image), no per-CA slice (a role limited to one CA sees nothing of it), and no renewal heatmap or key algorithm chart. The sources of the issuance chart are ACME, agent, and UI and API together.
- A report and an analysis are built at most once a minute per tenant, so a change can take a minute to show.
Upgrade notes
pki.dbmigration 0020 adds two indexes and changes nothing else; there is no configuration change and no change to the agent. An older binary refuses the migrated database, so going back needs the backup. Update the server as always; nothing has to be set up for the new pages.
0.3.7 - 2026-11-13
dns-01 in the agent. A deployment can prove its names through a DNS TXT record, so the agent can get wildcard certificates, certificates for a service without port 80 and for a host behind NAT. The agent writes the record through RFC 2136 (BIND, Knot, PowerDNS with a TSIG key) or the Cloudflare API, waits until it is visible, lets the ACME server read it and removes it again. The server is unchanged: it already only reads the record. The live test by the maintainer is pending.
Added
challenge: dns-01for a deployment, with a new top-leveldns_providersmap inagent.yaml:type(rfc2136orcloudflare),credentials_file,nameserverandzone(RFC 2136),ttl,resolver,propagation(authoritative, the default,resolverorwait),propagation_waitandpropagation_timeout. A deployment names its provider withdns_provider. A wildcard name (*.homelab.lan) is allowed only with dns-01, a dns-01 deployment takes nowebroot, andconfig checkreports every problem at once. A new deployment keycheck_namesets the server name of the local TLS check, which a deployment of wildcard names needs.- A credentials file of its own for the secret (a TSIG key, or a Cloudflare API token and optionally a
second token that may only read zones): YAML with strict keys, never in
agent.yaml, never on a command line or in the environment. The agent opens it without following a symbolic link and checks the open file: regular, mode 0600 or 0400, owned by root or the agent’s user, at most 4 KiB, in a directory that only root or that user can write; the path may not be under/home,/rootor/run/user,state_dir,pathsor a deployment target. It is read when an issuance starts, so a rotated secret works at the next renewal without a restart.myownpki-agent config checklists the state of every credentials file (ok,missing,wrong mode,wrong owner,unsafe directory,symbolic link,invalid, …) and exits 1 when a deployment uses one that cannot be used; it never prints a content. - RFC 2136 provider (the agent’s own, on
miekg/dns): one TSIG-signed UPDATE over TCP adds the record and one removes exactly this run’s value;hmac-sha256(default),hmac-sha384andhmac-sha512(hmac-md5andhmac-sha1are refused); a success is accepted only with an answer whose own TSIG verifies; the zone comes fromzoneor from an SOA lookup at the nameserver; a CNAME delegation of_acme-challengeis followed. - Cloudflare provider (lego’s, with the agent’s own HTTP client): the system trust store, TLS 1.2, a
30-second timeout and the
proxy:ofagent.yaml, neverHTTPS_PROXY. Error answers are cut to their codes and short messages; a failed delete, which lego only logs, is reported as a cleanup failure. - Propagation check and cleanup:
authoritativewaits until every authoritative name server of the zone has the record,resolveruntil the configured resolver has it,waitonly sleeps. The agent uses its own resolver (resolver, else/etc/resolv.conf) and never falls back to public resolvers. The record is removed on success, failure, cancellation and timeout. Each removal gets 30 seconds; while the agent is stopping, all removals of one issuance share 12 seconds, so that systemd does not kill it in the middle of the second record. A failed removal is logged and shown in the deployment’s Last error (dns-01 cleanup failed: a TXT record may remain at _acme-challenge.<name>; …, without provider text or secrets); it does not fail the issuance, the deployment staysdeployed, and the next pass without a cleanup problem clears the text. With Cloudflare the notice appears when the create request may have worked (a network error, an answer that is not the API’s JSON, a 5xx or a 2xx), not for a request the API refused with a 4xx. A challenge for a name that is not one of the deployment’s names is refused. deployment addflags:--challenge dns-01,--dns-provider,--dns-type,--dns-credentials,--dns-nameserver,--dns-zone,--dns-resolverand--check-name.--dns-typedefines a new provider entry; the same flags again change nothing and a different definition of an existing name is refused (“edit agent.yaml by hand”). A missing or wrongly protected credentials file is a warning, not an error, because a configuration tool may rundeployment addbefore the secret file exists.--dry-runshows the diff.- Salt formula: the seven options
dns_provider,dns_type,dns_credentials,dns_nameserver,dns_zone,dns_resolverandcheck_nameof a deployment, with refusals for a dns-01 deployment withoutdns_provideror withwebroot, adns_*option next to another challenge and a wildcard name without dns-01. The credentials file is not managed by the formula: place it withfile.managed(mode: 0600,show_changes: False) in the host’s own state. - Documentation: the page dns-01 and wildcard certificates and the operations guide §12.11; the pages on adding hosts, the command line, recipes, the Salt formula, best practices, troubleshooting, upgrading, ACME explained, split DNS, the FAQ and the glossary no longer say that the agent has no dns-01.
- Tests: an in-process DNS server with a zone store (
internal/testutil/dnsserver, imported by tests only) and a Cloudflare stub; the real agent against the real ACME server for a wildcard and its base name through RFC 2136 and through the stub, with the refusals (name missing in the agent’s names, domain without wildcards, a record that never appears, a wrong key) and an empty zone after every case; a conformance test of the agent against the BIND oftest/conformancein the jobACME conformance.
Changed
- Dependencies: no new module and no version change.
github.com/miekg/dnsv1.1.72 is now a direct requirement ingo.mod(it was indirect, through lego) and the lego packageschallenge,challenge/dns01,logandproviders/dns/cloudflareare used in addition to those of 0.3.0. lego’s own RFC 2136 provider is not used: it would bring a Kerberos stack for GSS-TSIG (12 modules in the Linux build) that this release does not offer. The agent binary grows by about 0.6 MB. The server binary carries neither lego normiekg/dns, checked by a test ofgo list -depsand a depguard rule.THIRD_PARTY.mdhas the details. agent runremoves every environment variable that starts withLEGO_,CLOUDFLARE_,CF_orRFC2136_, andSSL_CERT_FILEandSSL_CERT_DIR, and logs their names, so nothing outsideagent.yamland the credentials file configures a DNS provider, turns on lego’s debug output or chooses the system roots that the Cloudflare calls trust. The agent’s own connections use theca_bundleofagent.yaml. The hooks inherit the cleaned environment: they see none of these variables.- The optional ARI lookup of a renewal pass is skipped while an issuance runs (a dns-01 issuance can wait up to 15 minutes) and asked again at the next pass, so it no longer stalls the pass.
- The local TLS check of a deployment uses
check_name, else the first name that is not a wildcard, as its server name.
Fixed
- Restore instructions: run
restoreas root, thenchown.myownpki restorebuilds its work copy next to the data directory (in/var/lib) and renames it onto the data directory, so the service user, which cannot write to/var/lib, could not run it. The message ofinstall.shafter a failed update (rolled back … but it is not healthy either), the upgrade page (Roll back), the operations guide §6 and the CLI page now say to stop the service, move the old data directory away, create an empty one, runrestoreas root,chown -R myownpki:myownpki /var/lib/myownpkiand start the service. The program is unchanged.
Security notes
- A TSIG key or an API token decides who may change a zone, so it is handled like a private key: a separate
root-only file, checked on the open file, held in a type that prints as
[redacted]in every log line, error, report and%v, removed from error texts and wiped when the issuance ends. No secret reaches the report to the server (the Last error of a deployment),agent status, lego’s log or a panic text. - Give the credential the least it needs: a TSIG key that may only add and remove TXT records at
_acme-challenge.<name>(BINDupdate-policy), a Cloudflare token for one zone with Zone / Read and DNS / Edit. The pages say so; check the permission names against Cloudflare’s documentation. - The agent never asks the server about policy: the ACME server decides which names the agent may order. A
wildcard is covered only by the identical
*.pattern in the agent’s names, and an enrollment pass still grants the host name only. - A host that holds the key of a wildcard certificate can present itself as any name below it: give a
*.pattern only to the agents that need it. - An independent security review found no blocker; its six findings were fixed before the release. The
fixes cover the redaction of the build error of a DNS provider (it ran after the secrets were wiped), a
race between the wipe of a TSIG secret and a cleanup that was still running, the report of a failed
cleanup in the deployment’s status, a Cloudflare record that may remain although the agent holds no ID,
one shared time budget for the cleanups while the agent stops,
SSL_CERT_FILEandSSL_CERT_DIRin the agent’s environment, and an ARI lookup that queued behind a running issuance; a test now pins that a CNAME target outside a configuredzoneis refused.
Upgrade notes
- No migration, no change to the server’s behavior and no configuration change; going back needs no restore. Update the server first, as always.
- Update the agent before you add a dns-01 deployment. An agent of 0.3.6 or older refuses an
agent.yamlwithdns_providers,dns_providerorcheck_nameand does not start (unknown keys are errors on purpose). An existingagent.yamlworks with 0.3.7 unchanged. To go back, remove the dns-01 deployments and thedns_providersblock first.
Known limitations
- The live test by the maintainer is pending. Not tested against the real thing: the Cloudflare code
against the real API (CI uses a stub), a BIND
update-policyon the maintainer’s server (the CI test usesallow-update), the defaultpropagation: authoritativeagainst name servers on port 53 (CI usespropagation: resolver), the Salt formula’s dns-01 deployments on live minions, and macOS. - Not supported: GSS-TSIG (Kerberos, Active Directory DNS), other DNS providers, the legacy Cloudflare global API key, DNS updates over HTTPS or TLS, more than one provider per deployment, dns-01 for enrollment (a host without port 80 enrolls with a join token and one approval) and dns-01 in the Add host assistant.
- A killed agent (
SIGKILL, a power cut) can leave a TXT record behind, and the Cloudflare record IDs are kept in memory only. The record does no harm to validation; remove it by hand. There is no sweep. - A changed option of an existing dns-01 deployment in a Salt pillar is not applied (remove it through
absentfirst), as for every deployment.
0.3.6 - 2026-11-12
Salt formula: deploy/salt/myownpki/ installs the agent from a hash-checked package, enrolls it with an
enrollment pass, adds its deployments and keeps them in step, from one state.apply myownpki. The
server and the agent are unchanged. The maintainer tested it live on five enrolled Debian amd64 hosts: one
state.apply myownpki upgraded the agent from 0.3.2 to 0.3.5, and a second apply changed nothing. The
enrollment of a new host with a pass, rpm hosts and other Salt versions are not tested live.
Added
- Salt formula for the agent (
deploy/salt/myownpki/:init.sls,agent.sls,map.jinja,defaults.yaml,pillar.exampleand a README). The pillar ismyownpki:agent:version,server,acme_directory,root_sourceandroot_sha256,package.sha256per architecture (amd64,arm64), optionalpackage.sourceandpackage.format,webroot,low_ports,enrollment_pass,config_source,deploymentsandabsent. On a minion it stages the package (deborrpm, from the Salt file server or anhttps://URL) in/var/cache/myownpki-agent, checks its SHA-256 against the value from your admin UI and installs only from that local file, stages the root (whichsetupchecks againstroot_sha256), writes the enrollment pass to/root/myownpki-join-token(mode 0600, never shown in any output or put on a command line) only while the host has no agent certificate, runssetupuntil it has one, and then applies the deployments withdeployment addanddeployment removeor renders a completeagent.yamland runssandbox sync. A secondstate.applychanges nothing. A new package on an enrolled host runssetupagain, which is the update path. The formula never enables or starts the service (setupdoes when the host is admitted; the restart issystemctl try-restart), so a host that waits for an approval stays down until the next apply finds it approved. It fails closed: a missing required key, no hash for the minion’s architecture, an unknown key, option or format, a pass that is not a string,config_sourcetogether withdeploymentsand similar mistakes render one failing state,myownpki-agent-refused, and install nothing. Salt ignoressource_hashforsalt://sources, so the formula’s explicit check, not Salt, protects the install. It is copied from the tag or fetched with gitfs; it is not part of the release archives. - CI job
salt formula: renders every test pillar ofdeploy/salt/test/cases(Debian amd64 with a pass, RedHat arm64 withconfig_source, a minimal pillar, anhttps://source and nine refusals) withstate.show_highstate, checks which states exist and which must not, that the pass appears in no output, and runsstate.apply test=Truefor the cases a Debian runner can apply. It uses a pinned Salt 3008.5 (onedir, SHA-256 checked);bash scripts/ci/salt-test.sh --self-testruns the checks that need no Salt. It is not a required check.
Changed
- Container image tag
latest: every stable release now also movesghcr.io/der-felix/myownpki:latest(a pre-release gets only its version tag). Production setups should keep pinning the version:latestmoves with every stable release, and an update of a CA should be a decision. The exampledocker-compose.ymlstays pinned. low_portson enrolled hosts: a host that already has the systemd drop-in20-low-ports.confneedslow_ports: truein its pillar. Four of the five hosts of the live test did.- Documentation: the Salt page is rewritten around the formula (install, minimal and full pillar, the first and the second apply, a host waiting for approval, removing the pass, updating, changing or removing a deployment, the refusals, what is and is not tested); the operations guide has a new chapter 12.10; the pages on adding hosts, upgrading and troubleshooting name the formula.
api/openapi.yamlinfo.versionis 0.3.6.
Known limitations
- Tested live only on enrolled Debian amd64 hosts (the upgrade of the agent, and a second apply that changes
nothing). Not tested live: the enrollment of a new host with an enrollment pass (
setup --token-file, the removal of the pass file, a host that waits for approval),rpmhosts,arm64,config_sourceand the gitfs install. CI renders and applies the formula withtest=Trueunder Salt 3008.5 only. Try it on one minion first. - A changed option of an existing deployment in the pillar is not applied: the formula adds a deployment
only when
deployment listdoes not show the name. Remove it throughabsentfirst, or useconfig_source. - The agent’s state directory must stay
/var/lib/myownpki-agent; the formula looks foragent.crtthere.
Upgrade notes
- No migration, no configuration change and no change to the server or the agent. Update the server first, as always; going back needs no restore.
- Nothing uses the formula until you copy it to a Salt master. With it, updating the agents is a change of
versionandpackage.sha256in the pillar and astate.apply.
0.3.5 - 2026-11-12
Host onboarding: a new host goes from its name to a deployed certificate with an Add host assistant,
myownpki-agent deployment add writes agent.yaml and the systemd sandbox in one step, an enrollment
pass lets Salt, Ansible or a script enroll many hosts without a click per host, and the documentation
carries recipes for nginx, Postfix, Dovecot and Proxmox VE. The live test by the owner is pending.
Added
myownpki-agent deployment add NAME,listandremove NAME:addwrites a deployment intoagent.yamland the matching entries into the systemd drop-ins in one step, instead of two hand edits that showed up only asRead-only file systemafter the first failed renewal. It checks the result with the loader ofrunandconfig checkbefore it writes a byte, refuses a name that exists and the system directories (/,/etc,/usr,/var,/homeand others) as--dir,--webrootor--sandbox-write, keeps comments and the previous file (agent.yaml.bak), writes the drop-ins first so that a crash never leaves a missing entry, runsdaemon-reloadand prints the restart command (--restartdoes it; the agent is never restarted by surprise).--dry-runprints a diff ofagent.yamland both drop-ins and writes nothing.listprints the deployments (no root needed);removetakes one out again, keepspathsunless--prune-paths, and never touches deployed files or state. All exceptlistneed root, also with--dry-run, and agent 0.3.5.- The sandbox model and
sandbox sync:agent.yamlgets an optionalsandbox:block (write: [dirs]for directories only hooks write, such as/var/log/nginx, andlow_ports). The drop-ins10-paths.confand20-low-ports.confare derived fromagent.yamlby one piece of code thatsetup,deployment add|removeandsandbox sync [--dry-run] [--adopt|--prune]share.sandbox syncis what a configuration tool runs after it has rendered the file;config checkprints the effective sandbox, whether the drop-ins match and warnings for system directories.setup --acme-directory URLwrites the ACME directory into a newagent.yaml, so that the first deployment needs no hand edit. - Recipes:
deployment add --recipe nginx|postfix|dovecot|proxmoxfills files, owner, key mode, hooks, the local TLS check and the sandbox directories; every explicit flag wins. The nginx recipe rests on a setup the end-to-end tests run. Postfix, Dovecot and Proxmox VE are unverified examples, written from the documentation of each service and not yet run on a live host:deployment addsays so and lists what to verify, and the documentation says to try them on a non-production host first. The Proxmox recipe does not write into/etc/pve;pvenode cert setinstalls the files. - Enrollment pass: one secret (
mjp_…) that lets 2 to 25 hosts (default 10) enroll without a person looking at each one. An admin creates it with the second factor (Agents → Join tokens → New enrollment pass): a required label, 1 to 5 names or*.suffixpatterns that a domain must cover, up to 7 days (default 24 hours). It is shown once; the server keeps the hash. A pass admits a host only after the server’s http-01 check of that host passed (inverifiedandauto, never inmanual), grants the host its own name only, and is refused for a host that already has an agent; every enrollment that presents it takes a use, also when it then waits in the queue. When the pass does not count, the host waits in the queue andsetupprints why (the enrollment pass has no uses left (10 of 10) – ask an admin for a new one,the pass expired,the pass was revoked,the pass is not for …,an agent for … exists – a pass admits new hosts only). The list showsactive 3 of 10,used up,expiredorrevoked, the hosts each pass admitted, and a Revoke button; revoking ends use at once and leaves the hosts as they are. Automation uses it withsetup --token-fileand reads the result with a viewer API token.pki.dbmigration 0019 (additive). - Enrollment pass in REST:
POST /api/v1/agent-join-tokenstakeskind(tokenorpass) andmax_uses; the join token schemas gainkind,max_uses,use_count,state,revoked_at,revoked_byand thehostsa pass admitted; newPOST /api/v1/agent-join-tokens/{joinTokenId}/revoke(admin, second factor;409for one that cannot admit anybody any more);DELETEwithdraws only a token or pass nobody used. No API token can create or revoke a pass or a join token, whatever its role (403; tests pin this for every role); give automation a viewer token. A pass without a label, with more than five names or for a name no domain covers is422;valid_hoursabove 168 for a pass andmax_usesoutside 2 to 25 are400. Audit actionsagent.token.create(kind, uses, names, expiry) and the newagent.token.revoke; everyagent.enrollentry names the token, its kind and the use (3 of 10), and the reason when it did not count. The secret is in no entry and no log line. - Add host assistant: Agents → Add host (
/hosts/add, in the menu and as a button on the agent list). Step 1 asks host name, service (nginx, Postfix, Dovecot, Proxmox VE, certificate files only), challenge and webroot, up to four more names and the package; step 2 checks the names against your domains and offers the three ways to be admitted with the approval each one still needs (a single-use token for this host, an existing pass that covers it, or no secret); the commands appear once, never cached and never in a URL: the token file, the verified root and package download,setup, thedeployment addblock anddeployment list. The progress page/hosts/add/<host>follows the host and reloads itself every 10 seconds until the agent is online and every deployment is deployed: Host enrolled (waiting for the host, checking, waiting for your approval with Approve and Refuse for an admin, refused, expired, admitted), Agent online and one row per deployment (pending, deployed with serial and expiry, failed or rolled back with the agent’s last error, shortened and shown as text). Viewers and operators can open it and get the commands for no secret or a pass; the token and the approval need an admin with a fresh second factor. Agents → Install agent is unchanged. - For scripts:
GET /api/v1/agentsandGET /api/v1/agent-enrollmentstake?hostname=(exactly that host name), and the newGET /api/v1/agent-install(viewer) returns the facts for installing the agent without a browser: server URL, ACME directory, the root with its SHA-256 and the package files with theirs (404while the server has no packages). - Documentation: operations guide §12.7 Adding a host, §12.8 Enrollment passes and §12.9 Recipes, and an upgrade paragraph; the site has the page Recipes and rewritten pages for adding hosts and the Salt flow, which now uses an enrollment pass (the Salt formula is still planned for v0.3.6, and the page marks its sketch as an example to adapt).
Changed
- The systemd drop-ins come from one model (behavior change of
setup):10-paths.confis no longer rewritten fromstate_dirandpathsalone. See Fixed for what an operator sees after the update;setupon an existing host now reportsKept in 10-paths.conf …andAdded to 10-paths.conf: …. - The agent list has an Add host button next to Install agent, and Join tokens is now one page for
tokens and passes. The generic detail and form pages can show wizard steps and reload themselves (the
progress page uses a plain
<meta http-equiv="refresh">; the UI still ships no script for it). api/openapi.yamlinfo.versionis 0.3.5.
Fixed
setuplost the hand-made/var/log/nginxentry: it rewrote10-paths.conffromstate_dirandpathson every run, and the Update agent commands runsetup, so the entry that the operations guide tells nginx hosts to add was gone after the next update andnginx -tfailed withRead-only file system.setupnow keeps entries it cannot explain and says so.setupremoved20-low-ports.conf: without--low-portsand with webroot enrollment it deleted the drop-in that the nginx hooks need for a listener on port 443. The file is now left as found, and removed only for an explicitsandbox.low_ports: false; a file that differs from the packaged one is not overwritten.- A standalone deployment did not count: only the enrollment’s challenge decided about
20-low-ports.conf; a deployment withchallenge: standalonefailed to bind port 80. Any standalone challenge now installs it. - A fresh
agent.yamlcould not hold a deployment, becauseacme_directoryis required for one andsetupwrote none: passsetup --acme-directory, ordeployment add --acme-directory.
Security
- Changing the agent enrollment mode now needs a fresh second factor and is refused for an API
token. In
automode a host that passes the enrollment check is admitted without a person, so the mode weighs as much as approving an enrollment (which already needed step-up) – but an API token with the admin role could set it, because tenant settings needed no step-up. The new permissionagent.enrollment(admin only, step-up) guards exactly that change: in the UI the settings form asks for the code and then saves;PUT /api/v1/settingswith a differentagent_enrollmentanswers403step-up-requiredto a session without a fresh code and403forbiddento a token, and stores nothing. Saving the same mode, leaving the field out, and every other setting stay as they were, with a token too, and so does reading. Behavior change for scripts: one that switches the mode through the API stops working; change the mode in the UI. Thetenant.settingsaudit entry already named the old and the new mode. - The enrollment pass is bounded on purpose. It needs a person with the second factor to create and to revoke (no API token, whatever its role), admits only after a passed http-01 check, grants the host name and not the pattern, never replaces an existing agent, and ends at most 7 days after creation. A leaked pass still admits as many more hosts as it has uses left for names that match; the documentation says to keep it short, small and out of the pillar after the roll-out, and what to do if it leaks.
Upgrade notes
pki.dbmigration 0019 is additive: existing join tokens stay single-use tokens (a used one counts as one use), and no certificate, agent or enrollment changes. A restart leaves the server sealed, as always. Update the server first.- Agents of 0.3.4 keep reporting and nothing changes on them.
deployment,sandbox sync,setup --acme-directoryand thesandbox:key need the 0.3.5 agent; runsetuponce on every existing agent (Agents → Update) to get the fixes above: a hand-made/var/log/nginxin10-paths.confand an installed20-low-ports.confstay.myownpki-agent sandbox sync --adoptturns the kept entries intosandbox.write. - A script that sets
agent_enrollmentthroughPUT /api/v1/settingsnow gets403; change the mode in the UI. - Going back to 0.3.4 means restoring the backup from before the update, because 0.3.4 refuses the newer
pki.db, and a 0.3.4 agent binary refuses anagent.yamlwith asandbox:block (remove the block first).
0.3.4 - 2026-11-12
TLS scan: the server connects to the TLS endpoints you enter, reads the certificate each one serves and tells you when it is not the one this PKI issued – the renewal that never reached the web server. The live test by the owner is pending.
Added
- TLS scan (integration level 2): an admin enters the endpoints that matter (host, port, optional
SNI); the server connects to each one every 15 minutes, reads the certificate it serves and compares
it with what this PKI issued for the name. A scan is one TCP connection and one TLS handshake, then
close: no HTTP request, no client certificate, TLS 1.2 and 1.3. The chain is judged afterwards against
the CAs of this PKI (
trusted,incompletewhen the endpoint does not send the intermediate,untrusted). The host must be covered by a domain, the domain’s address policy applies at every scan, and only the ports ofmonitor.scan_portsare allowed. Limits: 100 endpoints, 4 workers, 10 seconds per scan (checking the chain afterwards has 5 seconds of its own), 4 addresses per name. Scheduling survives clock steps: an endpoint whose next scan lies more than twice the interval ahead is scanned at once. Only metadata of the served leaf is stored (fingerprint, serial, clipped subject and issuer, dates, chain state, TLS version) – never the certificate, a key, the names in it or the resolved address. Scans, evaluation and alerts work while the server is sealed.pki.dbmigration 0018 (additive). - Scan alerts:
scan_mismatch(the endpoint serves an older, a foreign or a wrong-name certificate, seen by two scans in a row once the newest certificate for the name isscan_mismatch_afterold; a revoked certificate is critical after two scans, with no waiting time),scan_expiry(the served certificate ends within themonitor.expiry_daysthresholds, unlesscert_expiryalready reports it),scan_unreachable(afterscan_unreachable_afterfailed scans in a row, so a reboot stays quiet; critical after 24 hours) andscan_chain(incomplete or untrusted chain). A name this PKI never issued shows Not issued by this PKI and raises no mismatch. The text of an alert never holds subject or issuer of the served certificate, which the remote host chooses. They count in the dashboard’s Open alerts figure and reach e-mail and webhook like every alert. - Endpoints page: the new page Endpoints (menu Overview) lists every endpoint with its state, last scan, served fingerprint (linked to the certificate when this PKI issued it), expiry, issuer and last error, and has Add an endpoint, Scan now and Remove. Subject and issuer are shown as text. A certificate of this PKI that has ended with nothing newer shows as Expired.
- REST:
GET /api/v1/endpoints(viewer),POST /api/v1/endpoints(admin; refusals are422with the field and the rule, never a resolved address),POST /api/v1/endpoints/{endpointId}/scan(operator;202,429withRetry-Afterwithin 60 seconds,409while scanning is off) andDELETE /api/v1/endpoints/{endpointId}(admin). New permissionsendpoint.manage(admin) andendpoint.scan(operator, admin); no step-up, because they grant no trust. Audit actionsendpoint.create,endpoint.deleteandendpoint.scan; refusals are audited too, and an endpoint ID that is not well-formed is “not found” after the role check. - Settings:
monitor.scan_interval(default15m,5m–24h, oroff),scan_timeout(10s,2s–30s),scan_ports(443, 465, 636, 853, 993, 995, 5061, 6514, 8443, 8883, 9443; 1–32 ports),scan_max_endpoints(100, 1–500),scan_unreachable_after(3, 1–10) andscan_mismatch_after(1h,5m–24h), each also asMYOWNPKI_MONITOR_SCAN_*. Out-of-range values failconfig checkand the start.config checkshows the TLS scan rows. - Operations guide: chapter 14, TLS scan – set-up in the UI and with
curl, every state and what to do, the alerts, settings, limits, the address ranges that are never scanned, roles, a troubleshooting table and an end-to-end check – and an upgrade paragraph for v0.3.3 to v0.3.4. - Documentation site: the guides, the operations guide, the release notes and the example
configuration are published at https://der-felix.github.io/MyOwnPKI/, built from
site/and the repository files by GitHub Pages; it has a page for the TLS scan.
Changed
- Stricter address guard (behavior change). The check that decides where the server may connect is
now one package (
internal/netguard) shared by http-01 validation, the agent enrollment check and the scan. It now also refuses, under every address policy: the IPv6 ranges that embed an IPv4 address or translate to one – IPv4-compatible (::/96), NAT64 (64:ff9b::/96,64:ff9b:1::/48), 6to4 (2002::/16), Teredo (2001::/32) and the IPv4-translated range (::ffff:0:0:0/96) – because a router or a client may translate them to the embedded address (64:ff9b::7f00:1could reach127.0.0.1); the non-global ranges192.0.0.0/24,198.18.0.0/15,192.88.99.0/24,fec0::/10,100::/64and5f00::/16; and an IPv6 address with a zone. A legitimate service in such a range can no longer be an http-01 target, an agent enrollment host or a scan endpoint. The documentation ranges (192.0.2.0/24,198.51.100.0/24,203.0.113.0/24,2001:db8::/32) stay allowed. IPv4-mapped addresses are judged as the IPv4 address they carry. The error texts of http-01 are unchanged. - Dependencies:
connectrpc.com/connect1.21.1,github.com/a-h/templ0.3.1100 (generated files regenerated),go.yaml.in/yaml/v33.0.6,modernc.org/sqlite1.61.0 (SQLite 3.54.1) andgolang.org/x/net0.62.0.
Fixed
- Agent commands with an absolute path: the guided installation and update commands ended with
a bare
myownpki-agent setup …, which failed withcommand not foundwhere root’sPATHlacks/usr/local/bin(seen on Debian 13), although the package had been installed. Every command the UI generates for root – guided install and update for deb, rpm and tarball, and the new join token page – now calls/usr/local/bin/myownpki-agent, as does the agent’s “not enrolled” log hint and docs/OPERATIONS.md. Packages, tarball and the systemd unit already use that path; a test keeps them in line. - Join token page runs setup: the page of a new join token showed
enrollandsystemctl enable --now, which cannot work on a fresh package or tarball host – onlysetupwritesagent.yamland installs the unit (Unit myownpki-agent.service does not exist). The page now shows the token file (by here-doc, not as an argument), the root download with its SHA-256 check andsetupwith the detected package format, like the guided installation.
Upgrade notes
- No action is needed: migration 0018 is additive, and the scan connects nowhere until an admin enters
an endpoint. A restart leaves the server sealed, as always. To keep the server from ever connecting to
the endpoints, set
monitor.scan_interval: off. - Roles: tenant admins add and remove endpoints, operators and admins scan now, viewers read. A role limited to one CA cannot see the endpoints, as with alerts.
- If a service of yours lives in an IPv6 range of the stricter address guard (NAT64, 6to4, Teredo, IPv4-compatible), http-01 validation and agent enrollment for it now fail with an address-policy error; use an address outside those ranges.
- Container installations need no new variables; the defaults of the
MYOWNPKI_MONITOR_SCAN_*keys apply. Add the variables only to change them (operations guide, §14.6). - Going back to v0.3.3 means restoring the backup from before the update: v0.3.3 refuses the newer
pki.db.
0.3.3 - 2026-11-12
Monitoring: the server checks itself every minute, shows what needs an admin on a new Alerts page and tells you by e-mail or webhook – also while it is sealed.
Added
- Alerts: a job evaluates the rules every minute and keeps one alert per condition (kind and
subject, severity, first and last seen, resolved) in system.db (migration 0014, additive). Kinds:
cert_expiry(a valid end-entity certificate ends and no newer valid certificate covers the same names; agent identity certificates never count; an expired one stays 30 days),ca_expiry,crl(the dashboard’s existing CRL states),sealed,agent_silent,agent_certificateanddeploy_failed.sealedopens after 10 minutes and turns critical when a CRL of an online CA is less than 24 hours from its next update, because a sealed server can’t sign a new one (RFC 5280 §5.1.2.5). Alerts rise from warning to critical, resolve when the cause is gone and never duplicate. The evaluation readspki.dband the seal state and decrypts nothing, so it works while sealed. Opened, raised, lowered and resolved alerts are logged once asalert. - Thresholds: the new
monitor:section (alsoMYOWNPKI_MONITOR_*):expiry_days(default14, 3),ca_expiry_days(90, 30),sealed_after(10m, 1m–1h),agent_silent_after(1h, 10m–24h) andrepeat_every(24h, 1h–168h). Out-of-range values failconfig checkand the start. - Notifications: the new
notify:section (alsoMYOWNPKI_NOTIFY_*) with an e-mail channel (smtp,securitystarttls/tls/none,username,password_file,from,to,ca_file;noneonly without a username) and a webhook (url,secret_file,ca_file). Channels and secrets live in the configuration and in files read at each send – not in the database – so a sealed server can still report that it is sealed. After each evaluation one digest per channel lists what opened, became critical, is still open afterrepeat_every, or was resolved; nothing is sent when nothing changed. E-mail is plain text overnet/smtp; the webhook is a JSONPOST, signed withX-MyOwnPKI-Signature: sha256=<HMAC-SHA256 of the body>, https only (http to loopback), 10 seconds, no redirects, honoringHTTPS_PROXY/NO_PROXY. Messages are queued in system.db and retried after 1 minute, 5 minutes, 30 minutes and 2 hours, then dropped and logged as an error (notification dropped after the last retry). While no channel is configured nothing is marked as notified, so the first message after configuring one lists every open alert. - Alerts page and dashboard: Alerts lists the open alerts (what to do, linked to the object) and those resolved in the last 7 days; admins get Send test message. The dashboard shows an Open alerts figure and lists critical certificate alerts, silent agents and failed deployments in Needs attention, with a link to the page.
- REST:
GET /api/v1/alerts(viewer and up,?state=open|resolved) andPOST /api/v1/notifications/test(tenant admin;409without a channel,429withRetry-Afterwithin 10 seconds of the last test). New permissionnotification.test(admin); each UI or REST test is audited asnotification.testwith the result per channel, never a secret. myownpki notify test [--channel email|webhook]sends a test message through the configured channels with the server’s configuration and secret files (run it as the service user).config checkshows the channels (e-mail by server, security and recipient count; the webhook by host only) and the thresholds in effect. A missing, unreadable or empty password, secret or CA file fails the check (exit 1); no channel at all only warns.- Operations guide: chapter 13, Monitoring and notifications – setup, secret files, container variables, the test, what each alert means and what to do, retries, troubleshooting and an end-to-end check.
Changed
- Alerts name the whole tenant, so reading them needs a tenant-wide role (viewer or higher). A role
limited to one CA gets
403from the API, has no Alerts entry and sees no alert block on the dashboard.
Fixed
- Agent packages in the container image: the image carries the agent packages under
/opt/myownpki/agent-dist, but nothing pointedagent.package_dirthere, so a container served none of them. The image now setsMYOWNPKI_AGENT_PACKAGE_DIR=/opt/myownpki/agent-dist. Installations with install.sh were not affected.
Upgrade notes
- No action is needed: migration 0014 is additive and
notify:is off by default. To get messages, configurenotify.emailand/ornotify.webhook(operations guide, chapter 13), restart and unseal. A restart leaves the server sealed, as always. - The first evaluation runs one minute after the start. A server left sealed for more than 10
minutes (for example after an update) opens a
sealedalert, and agents that stopped reporting openagent_silentalerts, as soon as that evaluation finds them. - Container installations: add the
MYOWNPKI_NOTIFY_*variables and mount the secret files read-only (operations guide, §13.3).
0.3.2 - 2026-10-29
A fix release: the server now serves the agent packages it installs.
Fixed
- Agent packages on the agent listener: a v0.3.1 server answered every
/agent-packages/download with 404, because it named the packages after its full version line instead of the release version. Guided agent installation therefore found no package, and the agent list did not mark older agents for update. No migrations; update the server, then the agents.
0.3.1 - 2026-10-28
Guided agent installation and packages, independent Windows trust confirmation, configurable step-up and protected-action continuation, and safe predecessor handling for UI, REST, ACME and agent deployments.
Added
- Independent root trust verification: the public trust page, authenticated CA details and guided
agent installation show authoritative SHA-256 and display-only SHA-1 for Windows certificate dialog
comparison. Root CA details expose an exact 64-hex SHA-256 automation value; pending CAs show no
empty fingerprints. Windows helpers no longer embed an expected hash: they compute both values and
import only after a matching independent
-ExpectedSha256or explicit interactive SHA-256 confirmation. Strict argument validation, changed/mismatched certificate refusal, quiet-mode fail closed behavior and elevated-child reauthorization prevent accidental or self-confirmed imports. - Guided Linux agent setup and packages: GoReleaser nFPM builds
.deband.rpmfor Linux amd64/arm64 with anoreplaceconffile, example systemd files and no maintainer scripts or autostart. The sharedmyownpki-agent setupverifies the independent root SHA-256, securely reads optional join-token files, writes configuration/units atomically, preserves custom configuration, polls pending enrollments and enables or restarts only enrolled agents. Tarballs and packages use the same implementation.install-agent.shis now a deprecated verified-download forwarding wrapper with default version 0.3.1. - Verified agent distribution and guided installation:
install.shstages the exact release’s.deb,.rpmand tarballs for both Linux architectures after checking every artifact againstchecksums.txt, and restores the previous complete distribution when a server update rolls back. Docker contains the same immutable files under/opt/myownpki/agent-dist;agent.package_dirselects the host/container directory. The agent listener allows anonymousGET/HEADonly for the manifest-listed artifacts and checksums, with exact-name, size, method, traversal and symlink protections; RPC authentication is unchanged. Agents → Install agent requires admin and fresh step-up and generates the full verified download/install/setup flow from independent UI root and package hashes. Its optional exact-host token is one-use, expires after 15 minutes, appears only in the one-time response and is consumed through a 0600 file, never argv, URLs or logs. Agent reports additively carry the installed format (pki.db migration 0015 and protobuf field 5), and strict SemVer update hints cover only older agents, including correct prerelease ordering and no suggested downgrade. - Configurable predecessor handling on renewal: choose immediate, delayed (1–30 days) or no
revocation, always with reason
superseded. Tenant defaults are independent for agent deployments and other certificates, with agent-wide, per-deployment and one-operation overrides. Current migration defaults are owner-confirmed (agent: immediately after confirmed deployment; other certificates: after seven days), recorded indocs/PHASE3.md§8.4. Non-agent ACME renewals require the same account and exact normalized name set, with an optional admin-audited per-account override in UI and REST; shared accounts should use one per host ornever. Agent actions start only at an accepted deployed report with exact proof: a served TLS fingerprint always wins, with the leaf fingerprint read back after the atomic write as the check-less fallback. A written fingerprint never replaces a missing TLS proof and legacy agents without either field cause no action. Exact account/order/name mapping and a real serial transition are required; failed, rolled-back, currently serving, foreign-account and ambiguous certificates are protected. Delayed agent plans start at server-recorded deployment time and repeated reports never duplicate or move plans. - Planned revocations: certificate lists/details and agent deployments show
Revocation planned, the certificate filter and dashboard cover plans due within seven days, and authorized users can plan, cancel or revoke now with step-up and audit. The scheduler executes due plans idempotently, including restart/unseal catch-up; CRL publication follows the committed transaction through the existing CRL jobs. Additive system.db migration 0013 and pki.db migration 0014 create no revocations or schedules on upgrade with unchanged serials. REST adds renewal policy fields, plan/cancel endpoints, planned timestamps and agent/deployment policy endpoints; legacyrevoke_previousremains supported. - Web action continuation after step-up: protected forms show a safe action summary and run once after the second factor, without asking for their inputs again. In-memory pending actions use 256-bit IDs in POST fields only, are bound to the session, user and credentials epoch, expire after five minutes independently of the configured proof lifetime, and are consumed atomically before the existing authorized handler. Replay, concurrent clicks, restart and business failure cannot repeat the action; downloads, one-time secrets and multipart legacy imports keep their response behavior. A new confirmation replaces the session’s previous one, global entry/archive budgets reject unbounded growth, and every store operation reclaims expired state. REST step-up responses and the OpenAPI contract are unchanged.
- Tenant-configurable step-up lifetime: tenant admins can choose 5, 10, 15, 30 or 60 minutes
(default 5) in Tenant settings or through the additive
step_up_minutesREST field. Changes are audited and apply only to new TOTP, recovery-code and passkey proofs; each session snapshots the lifetime, so existing proofs are unchanged. system.db migration 0012 adds the setting and per-session snapshots, with five minutes retained for existing sessions.
0.3.0 - 2026-10-25
The Linux agent: trusted enrollment, ACME issuance, atomic deployment with rollback, scheduled renewals and operator-requested renewal through the web UI and REST API. Linux packages and a hardened systemd installation complete the release.
Added
- ACME in the agent with lego v4.35.2 as a library: a local account key registered by thumbprint over mTLS, host-generated certificate keys and http-01 in standalone/webroot mode. Its own HTTP client ignores environment proxy and lego trust settings. Linked accounts are restricted to the active agent’s allowed names at order creation, finalization and issuance, including after an unseal; ordinary ACME clients keep their normal domain policy. Account key rotation preserves the binding. Existing accounts cannot be captured by a new thumbprint reservation; denials are audited. Re-enrollment with a new agent ID and the old ACME key stops with an operator-action message. pki.db migration 0012 enforces unique reservations and links.
-
nginx and file deployment profiles: contained, atomic PEM writes with owner/group/mode, shared test/reload hooks and a local TLS fingerprint check. Failed activations roll back the whole reload group; first-install failures remove the new files. Mixed groups test the restored configuration and reload existing deployments back to their old leaf when safe. A durable journal recovers an interrupted pass before new work, and local status shows deployment results. A failed rollback reports both errors without a retry loop.
- Agent listener (
agent.base_url,agent.listendefault:8444,agent.tls_issuer,agent.ca): Connect-RPC over TLS 1.3. Its certificate comes fromagent.tls_issuerlike the admin and ACME listeners’. Client certificates are accepted from the agent CA only – an intermediate with the EKU limit clientAuth only, checked bymyownpki config checkand at the start. Without a certificate only the enrollment answers; everything else is checked per request (a revoked or disabled agent is cut off at its next call). - Enrollment with proof of control (ADR-18): the agent makes its key and CSR on the host; the
server fetches a check file bound to that key from
http://<host>/.well-known/acme-challenge/(the agent’s own port 80, or the webroot of the web server already there – written only through anos.Root, so a symlink can’t redirect it) with the rules of the ACME validator. Modes per tenant (auto,verified– the default –,manual; Tenant settings,agent_enrollmentinPUT /api/v1/settings, system.db migration 0011). Join tokens: single-use, expiring, for name patterns, stored hashed, shown once. The queue holds at most 100 enrollments without a valid token – one with a token is never refused – and junk expires after 24 hours. - Agent certificates: 7 days, renewed by the agent at 2/3 with a new key (at most 6 renewals per agent and hour); an agent whose certificate expired enrolls again with the key of its last one. Revoking an agent puts its agent certificates on the CRL. pki.db migration 0011.
myownpki-agentenroll [--token-file FILE],run,status,config checkwith/etc/myownpki-agent/agent.yaml. It ignoresHTTPS_PROXY/HTTP_PROXY; a proxy only when agent.yaml names one.- REST API
/agents,/agent-enrollments,/agent-join-tokens(permissionagent.manage, admin with step-up), and the pages Agents, Enrollment requests, Join tokens. - Reports and renewal scheduling: a heartbeat every five minutes with jitter and one combined report after each deployment pass; 2/3-lifetime scheduling with ARI support, persistent failure backoff and a silent-agent indicator after three missed reports.
- Renew now and Withdraw: operator/admin actions in the UI and REST API wake the agent through a bounded long-poll. Versioned acknowledgements prevent stale reports from clearing newer requests; CSRF, role checks and audit entries cover both actions. Agent pages show deployments, disallowed names and removed configurations. The certificate filter hides agent identity certificates by default, while deployment certificates stay visible.
- Linux packaging:
install-agent.sh, a hardened root systemd unit with explicit writable-path drop-ins, and reproducible Linux amd64/arm64 agent archives. The script verifies the downloaded archive against the signed checksum file, never overwrites agent.yaml and updates without self-update magic. Theagent end-to-endCI job runs real ACME/agent flows and nginx rollback, measuressystemd-analyze security, and checks protobuf compatibility withbuf breaking. - New modules (owner 2026-10-24):
connectrpc.com/connectv1.21.0,google.golang.org/protobufv1.36.12; buf v1.73.0 as a pinned development tool (THIRD_PARTY.md).
Changed
- Reports and desired-state watches have per-agent rate limits (60 and 120 per 10 minutes) to protect the SQLite writer and audit log from a compromised agent. The dashboard excludes agent identity certificates from ordinary 30-day expiry views and warns per active agent when its current certificate has less than 48 hours left.
- pki.db migration 0013 stores reported deployments and renewal requests. The release now has 18
assets, including the two agent archives and
install-agent.sh.
Fixed
- Enrollment without a matching domain stores a clear failed check instead of panicking; the service and Connect-RPC both return the pending enrollment with that explanation.
- Handler panics are logged at error level with the RPC procedure and stack on the agent listener; HTTP server panics are also logged at error level on the admin and ACME listeners. Panic messages remain visible when the configured log level is info, while TLS handshake noise stays at debug.
- Domain creation and editing require a signing CA whose EKU limit allows serverAuth, in the form and in the service and REST API. New forms start with “Please select” instead of the first CA.
install-agent.shno longer claims that a token file was kept when enrollment ran without one.- nginx test and reload hooks need
20-low-ports.conffor listeners below 1024, including webroot mode.install-agent.sh --low-portsinstalls that capability explicitly. - Failed hooks include sanitized, bounded stderr in the agent log and deployment
last_error: valid UTF-8 at every boundary, the final diagnostic lines preserved, Unicode format characters removed and the complete error within the server limit, including rollback failures. - Renewal requests appear as status badges with a separate Withdraw action, deployment tables wrap without horizontal overflow, and removable agent names have a per-row Remove action instead of a separate form.
- The Windows trust-page text requires comparing the root fingerprint with an independently obtained value from the admin UI or an administrator, not with the file that carries the root itself.
- Agent end-to-end CI runs
nginx -ton port 443 under the real systemd unit, first proving the failure without the low-port drop-in and then success with it. The exported nginx rootfs has the package’s temporary directories, and the negative-control log is written withtee.
0.2.2 - 2026-10-24
The server can issue the TLS certificates of its own listeners from one of its intermediates, so
ACME works without a reverse proxy; a daily backup timer for systemd installations; and a public
trust page that hands out the root certificates. Without tls_issuer the listeners keep their
bootstrap or configured certificate, and the proxy way for ACME stays the default. Upgrade from 0.2.1 with install.sh: run the 0.2.2 install.sh as
before – backup, new binary, migration (system.db 0010), health check; it now also installs the
daily backup timer and enables it once (--no-backup-timer keeps it off). An installed
public-port-80.conf stays. This install.sh installs 0.2.2 and later only; for an older release
use that release’s install.sh. Docker installations: back up, pull the 0.2.2 image, start; the
backup timer is for systemd installations. The trust page appears at / of the public listener in
both cases – switch it off in Tenant settings if you don’t want it.
Added
- The server issues its own TLS certificates from a CA of the instance:
admin.tls_issuerfor the admin listener (the names ofadmin.tls_names),acme.tls_issuerfor the ACME listener (the host ofacme.base_url). Through the normal CA engine – preset server, ECDSA P-256, the CA’s name constraints, an audit entry by system – for 90 days at most (never beyond the CA’s maximum or end), renewed at 2/3 and at once after a revocation, a name change or a CA renewal. Issued only while unsealed; key and fullchain lie in<data_dir>/tls/(key 0600), so a sealed restart serves them. When one expires while sealed, the listener falls back to its bootstrap certificate with a warning in the log and on the dashboard; the switch needs no restart. A root CA astls_issueris refused with the fix (“CA root is a root – name the issuing intermediate …”) at the start, on the dashboard and inmyownpki config check(a warning when the CA database is readable); the listener then serves its bootstrap certificate. docs/OPERATIONS.md §2.4. -
ACME without a reverse proxy: with
acme.tls_issuer,acme.listenspeaks TLS itself (e.g.":443"); every client may connect, the peer is the client address for rate limits and audit,acme.trusted_proxiesandX-Forwarded-Forare ignored. The proxy way stays the default. docs/OPERATIONS.md 10.10. - Daily backups:
myownpki-backup.serviceand.timer(daily, random delay up to an hour, missed runs caught up), installed and enabled by install.sh (--no-backup-timer,--backup-keep N, default 14). Each run writes/var/backups/myownpki/<UTC time>, verifies it and prunes; the unit has the server’s sandbox and no network. docs/OPERATIONS.md §6.1. myownpki backup scheduled DIR --keep N(the timer’s run: backup, verify, prune) andmyownpki backup prune DIR --keep N [--dry-run]. Prune deletes only complete scheduled backups (named like20261025T003512Z, a validmanifest.json, exactly the listed files) – never install.sh’s update backups, a backup that failed its check (…-unverified) or anything else.- Backup warnings on the dashboard: a
…-unverifiedbackup in/var/backups/myownpki, or a newest timer backup older than 48 hours (timer off or failing) – once the timer is installed or has written a backup. The server only reads the directory; its unit needs no new rights. docs/OPERATIONS.md §6.1. - Public trust page at
/of the public listener, without sign-in: every active CA (roots first) with name, common name, validity and SHA-256 fingerprint, DER and PEM downloads, guides for Windows, macOS, iPhone/iPad, Android, Linux and Firefox. No JavaScript, a strict CSP, cached. On by default; Tenant settings ortrust_pageinPUT /api/v1/settingsswitch it off (system.db migration 0010). docs/OPERATIONS.md chapter 11. - Windows helpers per root on the trust page: a
.cmd(Batch/PowerShell in one file, ASCII, CRLF) for this computer (asks for administrator rights) or my user only; it checks the SHA-256 of the embedded root before the import and stops if the file was changed;/checkonly checks, an unknown switch never imports. /ca/<name>/<generation>.pem: the CA certificate as PEM.install.sh --acme https://NAME/acme(with--acme-ca, defaultissuing) writes the ACME keys for its own TLS into a new config and installs the drop-in for port 443.
Changed
myownpki healthcheckaccepts the bootstrap and the issued admin certificate (withadmin.tls_issuer);myownpki config checkshowstls_issuerand the ACME mode.- The drop-in
public-port-80.confis nowlow-ports.conf– for every listener on a port below 1024 (--public-port-80,--acme). An installedpublic-port-80.confkeeps working and stays; the archives still carry it. - CI: the install job installs with
--acme, unseals through the API and reads the ACME directory over TLS with the root as trust anchor;systemd-analyze securityis measured with the drop-in and must stay below 2.0. The conformance job runs lego and certbot directly against the own TLS listener.
0.2.1 - 2026-10-24
Install and update MyOwnPKI as a hardened systemd service with one command: install.sh
(docs/OPERATIONS.md §2.1) – no Docker needed. The server itself is unchanged. Docker
installations: nothing changes – back up, pull the 0.2.1 image, start, as before. Upgrade from
0.2.0 without Docker: as before (backup, replace the binary, start); or run install.sh once – it
keeps an existing /etc/myownpki/config.yaml and /var/lib/myownpki.
Added
install.sh– installs or updates MyOwnPKI as a systemd service with one command, on Linux with systemd (amd64, arm64); a release asset of its own (listed in the signedchecksums.txt) and in every archive. It checks the archive’s SHA-256 and, when cosign is installed, the Sigstore signature before it installs anything; creates the usermyownpki,/var/lib/myownpkiand – only if there is none –/etc/myownpki/config.yaml(admin listener on the own /24s, the names fromhostname -fand the own addresses, or--tls-names,--admin-listen,--allowed-networks); enables the service without starting it and prints the next commands. An update makes a backup andbackup verifyfirst and puts the old binary back – and ends with an error – unless the service comes back healthy and runs the new version.--archiveinstalls from a local archive (machines without internet). docs/OPERATIONS.md §2.1.- systemd unit
deploy/systemd/myownpki.servicewith a strict sandbox (no capabilities, read-only system except the data directory, private users and devices, a system call filter;systemd-analyze securityrates it 1.2, “OK”), and the drop-inpublic-port-80.conffor the public listener on port 80 without a reverse proxy (--public-port-80).
Changed
- The release has 13 assets (
install.shadded); the archives also hold the unit, the drop-in andinstall.sh.
0.2.0 - 2026-10-23
The ACME server (phase 2 of docs/PLAN.md). Upgrade: back up, replace the binary, start – the
migrations run at start; ACME stays off until acme.base_url is set, and it runs only behind a
reverse proxy that ends TLS. Read chapter 10 of docs/OPERATIONS.md before switching it on.
Added
- ACME server (RFC 8555) for certbot, acme.sh, lego, Traefik, Caddy and other clients: accounts (with key change and deactivation), orders, http-01 and dns-01 challenges, finalize, certificate download with the chain, revocation (by the account, by an account that holds the names, or with the certificate’s own key). Issuance goes through the same policy engine as the UI.
- Domains (ACME → Domains,
/api/v1/domains): which names ACME issues for, the signing CA, the challenges, wildcards (dns-01 only), the preset and validity (default 90 days), which addresses http-01 may reach, and IP ranges for IP certificates. - Approvals: per domain open, approved accounts (default) or new names; queues for accounts and names in the UI, EAB keys (external account binding) shown once, dashboard notices for what waits.
- ARI (RFC 9773): clients ask when to renew – the window opens at 2/3 of the lifetime; a revoked certificate is renewed at once; renewals name the certificate they replace.
- Rate limits as tenant settings: new orders per account, failed validations per name, new
accounts per client address (IPv6 per /64);
429with the realRetry-After. - A sealed server keeps taking orders: certificates of finalized orders are issued at the unseal.
- Configuration
acme.base_url,acme.listen(default127.0.0.1:8081),acme.trusted_proxies,acme.resolver; examples for Caddy, nginx and Traefik in docs/OPERATIONS.md. - ACME → Orders lists the newest orders with their result.
- Tested in CI against real clients: lego, certbot (http-01, dns-01 with certbot-dns-rfc2136 against BIND, revocation) and acme.sh; Pebble as a control.
Changed
PUT /api/v1/settingstakes an optionalacme_limits; left out, the stored limits stay (clients of 0.1 keep working). The answer always contains it.- Table cells in the UI can wrap long values (error details, contacts).
Security
- ACME runs only behind a reverse proxy that ends TLS; the plain-HTTP listener answers only the
addresses in
acme.trusted_proxies, and no URL is built fromX-Forwarded-Host/-Proto. - JWS and JWK in own code with the standard library only: a fixed list of algorithms (ES256,
ES384, RS256 with at least 2048 bits, EdDSA), strict parsing (duplicate members,
b64:false, unknowncritandnone/HS* refused); fuzz-tested. - The validator resolves once and connects to exactly the checked address (no DNS rebinding), never to loopback or link-local, only to ports 80/443, follows redirects only within the order, and never tells the client what a target answered – it can’t be used to read the inside network.
- EAB keys are stored encrypted with the key ring and overwritten when used.
- Unseal refuses weak key derivation parameters: a key ring whose stored Argon2id parameters
are below t=4, 256 MiB, p=4 is refused (audited as
denied, reasonweak_kdf). Every release so far stored exactly these values, so existing installations are not affected; weaker values come only from test builds. Way out: unseal with the recovery key, ormyownpki passphrase reset, and set a new passphrase – it is stored with the production values.
Fixed
- Release notes on GitHub: the release draft had an empty text, because GoReleaser’s changelog step – the one that reads the notes from CHANGELOG.md – was disabled. (The v0.1.0 notes were added by hand.)
0.1.0 - 2026-10-22
The first release: MyOwnPKI replaces the pki.sh Bash suite in the browser (phase 1 of
docs/PLAN.md). Start with docs/OPERATIONS.md.
Added
- CA engine: root and intermediate CAs (online, or an offline root with the signing bundle for
OpenSSL), CA renewal with a new key, retirement and revocation; leaf certificates from a CSR or
with a server-generated key; built-in presets (server, client, server-client, S/MIME, code
signing, timestamping, OCSP, custom) ported from
pki.sh; name constraints, CA limits and the policy checks of PLAN §6 – extensions always come from the preset, never from the CSR. - Revocation and CRLs: revocation with RFC 5280 reasons (keyCompromise blocks the key for later
requests), CRLs signed at once and renewed in the background, the offline root’s CRL uploaded;
CRLs and CA certificates on the public listener, including the URLs of imported
pki.shCAs. - Key protection: every CA and certificate key encrypted (AES-256-GCM under a root key wrapped
with an Argon2id key from the unseal passphrase, plus a recovery key); the server starts sealed;
seal and unseal audited;
myownpki passphrase changeandpassphrase reset(with the recovery key). - Audit log: hash-chained instance and tenant logs with signed checkpoints;
myownpki audit verifyand the audit pages. - Sign-in: password (Argon2id, common-password check) plus TOTP and recovery codes, passkeys as
second factor when
auth.passkey_originis set (through the API only, see below), step-up for dangerous actions, lockout and throttling, roles per tenant (viewer, operator, admin) and instance admins; API tokens. - REST API
/api/v1(OpenAPI 3.1 inapi/openapi.yaml, 58 operations, RFC 9457 problems). - Web UI: dashboard, certificates (issue wizard, detail, revoke, renew, download, export as PKCS#12 or PEM with “delete the key afterwards”), CAs including the offline signing flow, presets, users and roles, tenant settings, audit log, legacy import, own account; light and dark theme; no inline styles or scripts (strict CSP).
- Legacy import of a
pki.shdirectory (myownpki import legacy, or an archive upload in the UI): serial numbers, revocations (time and reason) and CRL numbers stay identical; dry run first. - Backup and restore:
myownpki backupwhile the server runs,backup verify,restorewith hash, integrity, audit-chain and name-index checks. - Operations:
myownpki init(interactive,--stdinfor automation,--demo),myownpki healthcheck,config check,db status/db migrate,user unlock/reset-2fa; a self-signed bootstrap certificate for the admin listener with its fingerprint in the log. - Release: archives for linux/amd64, linux/arm64, darwin/arm64, windows/amd64 and
freebsd/amd64 (pure Go, reproducible), SPDX SBOMs, a checksum file signed keyless with cosign,
and the container image
ghcr.io/der-felix/myownpki(distroless, non-root, signed);deploy/docker-compose.ymlfor a single server.
Security
- Built with Go 1.27.3 (fixes GO-2026-6809 / CVE-2026-111943:
crypto/x509now refuses known compromised intermediate certificates when verifying a chain).
Known limitations
- No ACME server yet (v0.2), no agent and no monitoring yet (v0.3) – see the release plan in docs/PLAN.md §10.1.
- Passkeys work only through the REST API in v0.1 – registering one, and using it to sign in
or to confirm an action (
/api/v1/auth/webauthn/*,/api/v1/users/me/webauthn). The web UI has no passkey button yet: it needs a small browser script that is not approved yet (ADR-04). The UI lists a user’s passkeys and removes them; sign-in in the browser uses the authenticator app (TOTP). - One tenant (
default); the tenant UI comes later.