Repository navigation
Releases
MyOwnPKI v0.3.9
The trust portal. The page at / of the public listener now guesses the visitor's system from the browser, lets the
visitor choose another one, and shows the steps and commands that install a root there; iPhone, iPad and Mac get a
configuration profile. The root's page in the admin UI shows the portal's address and a message to send to users.
An agent host can put the root of this PKI into its own system trust store on request, by command or with the Salt
formula. The release adds no dependency and no migration. The live test by the maintainer is pending.
Added
- Trust portal (
GET /of the public listener, internal packagetrustkitfor the generated parts): the page
starts with a warning that it comes over plain HTTP, so nothing on it proves itself, and says where to get the
SHA-256 fingerprint another way (the admin UI,myownpki ca show NAME,openssl). Then a table of the roots
(name, common name, validity, DER and PEM downloads) with each root's SHA-256, the list of systems, the steps for
the chosen system and a table of the intermediates (downloads only: servers send them, nothing is installed). - Platform detection from the
User-Agent(first 512 bytes, a fixed table, no regular expression: iPhone, iPad
and "like Mac OS X", Android, Windows, macOS, Debian and Ubuntu, Fedora, Red Hat and CentOS, Linux or X11 as both
Linux blocks, ChromeOS as nothing; Firefox is a flag) and a manual choice with/?os=<value>:windows,macos,
ios,android,debian,fedora,firefox,javaorall. Any other value counts as not given. - Install blocks per platform, generated per root from the database: Windows (the DER as
.cer, the certificate
dialog, and PowerShell for the current user and for all users), macOS (profile and a Terminal block), iPhone and
iPad (profile, and the second step under Certificate Trust Settings), Android (user CA store; apps that target
API 24 or higher ignore user CAs), the Debian and RedHat families, Firefox (security.enterprise_roots.enabled
on Windows and macOS only) and Java (keytoolwithout-noprompt). Each block with a command says that the
embedded SHA-256 only checks the download against the page. - Configuration profile (
/trust/<name>/<generation>/profile.mobileconfig, roots only): unsigned, only the public
certificate, byte-identical for the same root, removable by the user,application/x-apple-aspen-config.
Devices show it as Not Signed. - Admin UI: a root's page shows a Trust portal row (the address, or why there is none) and a plain-text
Message for your users with the address, the root's SHA-256 and the iPhone step. - Agent:
system_trust. Off by default.myownpki-agent trust install --root-sha256 HEX [--no-record],
trust removeandtrust status [--quiet],setup --system-trust, theagent.yamlkey
system_trust: {root_sha256: "<64 hex digits>"}and asystem trustline inconfig check. Root only, never part
ofrun. Debian-family and RedHat-family hosts, one fixed file and one fixed command per family, the pin checked
againstca_bundle(one self-signed CA certificate) before anything is written, a failed command rolled back.
config checkexits 1 only when the key does not matchca_bundle. - Salt formula: the pillar key
system_trust(a boolean, defaultfalse) and the state
myownpki-agent-system-trust;--no-recordwithconfig_source; the formula refuses a value that is not a boolean,
trueon a minion that is not Debian or RedHat family, andtruewith a version below 0.3.9.falseor absent
creates no state and the formula never removes the root. - Screenshots tool: a
publicview kind (the demo instance's public handler) for the portal shots, and-only
now also selects the standard shots by name. - Documentation: the site page The trust portal, operations guide §11.2, §11.3 and §12.12, two pictures in the
tour, and the notes in the FAQ, glossary, guide, CLI and Salt pages.
Changed
- The
trust_pagesetting keeps its name, default and API field. Its label is now Trust portal at / of the public
listener (CA certificates, fingerprints, install steps per platform), and its help text names the portal page, its
?os=choice, the profile and the Windows helpers and says that/ca/and/crl/stay served. - File names: downloads, the profile, the Linux files and the Windows helpers carry the first 8 hex digits of the
certificate's SHA-256 (<name>[-<generation>]-<8hex>.crt,myownpki-<name>[-<generation>]-<8hex>.crton Linux,
…-trust-computer.cmd), so two certificates never share a name. The/ca/URLs are unchanged. - The page: one block per system instead of one section per CA with a guide for the first root only. The by-hand
Windows step is now the certificate dialog and two PowerShell blocks instead ofcertutil -addstore; the text of the
Windows helpers is as before. The Linux commands download, check the hash and install in one&&chain instead of
sudo cp. - Caching: the page sends
Vary: User-Agentand isprivate, max-age=300when the browser told it something,
public, max-age=300otherwise; the profile and the helpers arepublic, max-age=900..btn-rowhas a bottom margin. api/openapi.yamlinfo.versionis 0.3.9, and the description oftrust_pagefollows the web UI (description only).
Security notes
- A public page without a sign-in shows only public data: CA certificates, their fingerprints and text. A request
can influence two things: the platform guess (a row of a fixed table) and the?os=value (an entry of an
allow-list). Nothing of theUser-Agentor the query is written into the page, a header, a file name or a command
(tests plant a canary in both). TheHostheader is not used: addresses come from the CA's public base URL. - No half-checked command. The base URL must pass a conservative pattern and the CA name its own check; otherwise
the block shows no command and says to compare by hand. Variables in a command are hashes, validated names and that
address. The Linux, macOS and Java chains are joined with&&and start in a newmktemp -ddirectory, so a failed
check stops them and another local user cannot swap the file between the check and the install. - The page does not claim more than it can. It says first that nothing on it proves itself, and that the hash in a
command only checks the download against the page. The Windows helpers still contain no expected value. - Switched off means 404, the same as for a path that never existed;
/ca/and/crl/stay. None of the portal's
own routes redirects (Go's router still answers 307 for an unclean path before a route runs). A request writes no
state, no log line and no audit entry. The Content Security Policy has noscript-src. - Agent: the install is a command run by root, not part of the service; the sandbox of the unit and its drop-ins
are unchanged. The checked bytes are the ones written; the files and the command are fixed; no shell, a cleaned
environment, a time limit, umask 022; symbolic links are refused;removedeletes only a file that holds the pinned
root. What the option allows is stated: every program on the host then trusts that root for every name. - No new dependency;
go.mod,go.sumand the guard of the server binary's imports are unchanged. - An independent review found no blocker, and its findings were fixed: a switched-off page that differed from a
404, a helper file name built from the request's text, colliding file names, commands that ran in a shared directory,
the trust command run again after a failure and for unchanged files, the hook's umask and the removal command, and
a root page that offered an address for a root the portal does not list.
Known limitations
- The live test by the maintainer is pending. How iOS, iPadOS, macOS, Android and Firefox show the profile and the
menus, what a profile without a scope does on a Mac, whether Firefox on Linux honorsenterprise_roots, and
update-ca-certificateson a real host were not tested on devices; the menu names on the page are the vendors'
documented ones and may differ. The page is corrected after the live test. - The page cannot prove itself. It comes over plain HTTP (the public listener has no TLS), so the fingerprint must
be compared with a value from another source, and the hash in a command checks the download only against the page. - No rate limit on the public listener and no counters; limit in the reverse proxy when it is reachable from the
internet. - A shared cache may serve the profile and the helpers for up to 15 minutes, and a
publicpage for up to 5, after the
portal is switched off. The directory a pasted command creates withmktemp -dstays behind. - Detection is a guess: an iPad that asks for the desktop version looks like a Mac, a Linux without a distribution
shows both families, ChromeOS shows the list. The profile is unsigned (Not Signed); there is no MDM payload, no QR
code and no installer script. There is no way to hide one root from the portal. system_trustsupports Debian-family and RedHat-family Linux only, adds the host's own root only, and a renewed root
needs a newtrust installaftersetup. As non-root,config checkcannot read a 0600ca_bundleand does not
check the key against it. The Salt render has run in CI only.
Upgrade notes
- No migration and no configuration change;
myownpki db statusis unchanged, so going back needs no restore. - Update the agents before you enable
system_trust: an agent older than 0.3.9 refuses the key and does not start.
Update the server first, as always. The new file names and headings change the page, not the/ca/URLs.
MyOwnPKI v0.3.8
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 in
api/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. Expectorphaned_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 inlinestyleattributes, 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.
MyOwnPKI v0.3.7
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_waitand
propagation_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 inagent.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 theproxy: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 without
dns_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_or
RFC2136_, andSSL_CERT_FILEandSSL_CERT_DIR, and logs their names, so nothing outsideagent.yaml
and 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 restor...
MyOwnPKI v0.3.6
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 (whichsetup
checks 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, runs
setupuntil 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 is
systemctl 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 ignores
source_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) with
state.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:latest
moves with every stable release, and an update of a CA should be a decision. The example
docker-compose.ymlstays pinned. low_portson enrolled hosts: a host that already has the systemd drop-in20-low-ports.confneeds
low_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 whendeployment listdoes not show the name. Remove it throughabsentfirst, or use
config_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.
MyOwnPKI v0.3.5
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 into
agent.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 of
agent.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 that
setup,deployment add|removeandsandbox sync [--dry-run] [--adopt|--prune]share.sandbox sync
is 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 URL
writes 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,expiredor
revoked, 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) and
max_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; every
agent.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, the
deployment 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_dirand
pathson 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 with
Read-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 and
setupwrote 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. Inautomode 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 permission
agent.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_enrollmentanswers403
step-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 t...
MyOwnPKI v0.3.4
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) and
DELETE /api/v1/endpoints/{endpointId}(admin). New permissionsendpoint.manage(admin) and
endpoint.scan(operator, admin); no step-up, because they grant no trust. Audit actions
endpoint.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 fromsite/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 reach
127.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) and
golang.org/x/net0.62.0.
Fixed
- Agent commands with an absolute path: the guided installation and update commands ended with
a baremyownpki-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
enrolland
systemctl enable --now, which cannot work on a fresh package or tarball host – onlysetupwrites
agent.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 and
setupwith 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, setmonitor.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.
MyOwnPKI v0.3.3
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_certificateand
deploy_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(default
14, 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 with
X-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) and
POST /api/v1/notifications/test(tenant admin;409without a channel,429withRetry-After
within 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 gets403from 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 asealedalert, 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).
MyOwnPKI v0.3.2
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.
MyOwnPKI v0.3.1
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 against
checksums.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_dir
selects 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 reasonsuperseded. 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 or
never. 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_previous
remains 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 additivestep_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.
MyOwnPKI v0.3.0
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 fromhttp://<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_enrollmentin
PUT /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/protobuf
v1.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 andinstall-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.