Skip to content

Release Notes

MazeVault Platform Version History

Document Version: 1.0.61 Last Updated: 2026-10-28


Version 1.0.61

Release Date: Unreleased

Distribution Changes (action required)

  • PKCS#11 HSM devices need HSM_PKCS11_MODULES (set it before upgrading) — The server now loads only the PKCS#11 libraries the operator lists in the new variable HSM_PKCS11_MODULES (comma-separated absolute paths, as seen inside the backend container). It is empty by default, which refuses every PKCS#11 HSM device: after the upgrade the connection test, the health monitor and key routing report such a device as unreachable until its module is listed, and the log lists the affected devices at startup. Customers who use PKCS#11 devices must set the variable (.env, Compose or Helm backend.config) before upgrading.
  • Agent version policy replaces the agent version environment variables — Which agent version each agent is offered is now set by the version policy (Deployment → Agents → Version policy). AGENT_VERSION, AGENT_MAX_ALLOWED_VERSION and AGENT_ROLLOUT_PERCENTAGE are deprecated: they seed an empty policy once on the primary's first start (later changes to them only log a deprecation warning). Pinned values become targets and the ceiling, so agents running a higher version are offered a downgrade to them — clear the targets/ceiling in the policy or set a minimum rollback version if that is not wanted. Gateways follow the primary's policy. The new agent rollback floor min_rollback_version limits how far an agent may be rolled back; it can be cleared with reset-min-rollback-version.
  • Images, Helm chart and Compose kit move to the MazeVault Harbor — Starting with this release, MazeVault is distributed only from registry.mazevault.com: images registry.mazevault.com/mazevault/mazevault-{backend,frontend,edge,docs,ocsp,init-certs}:vX.Y.Z, the Helm chart oci://registry.mazevault.com/mazevault/mazevault-customer (version X.Y.Z), and the Compose kit (installer, upgrade/rollback scripts, docker-compose.yml, .env.example, SHA256SUMS) as OCI artifact registry.mazevault.com/mazevault/compose-kit:vX.Y.Z, downloaded with oras pull (run its scripts with bash, ORAS does not keep the executable bit). Each customer receives a pull-only robot account; there is no GitHub token and no raw.githubusercontent.com download any more. v1.0.60 and older remain on GHCR (ghcr.io/mazevault); existing installations keep their IMAGE_REGISTRY until it is changed explicitly (install-mazevault.sh --registry, or the chart's image.repository values).
  • The online installer uses the kit's docker-compose.yml — install-mazevault.sh no longer writes its own embedded copy of the compose file; it installs the docker-compose.yml shipped next to it in the Compose kit (as install-mazevault-offline.sh already did) and stops with an error when it is missing, so run it from the extracted kit. Online installations re-run with this release therefore pick up the kit's settings, e.g. MAZEVAULT_ENV_CANONICAL_ENFORCE (default false); MAZEVAULT_CUSTOMER_NAME, MAZEVAULT_CUSTOMER_EMAIL and MAZEVAULT_COMPANY_ID must be non-empty in .env (the installer always generated them). The kit's compose file gains the backend health check the installer's copy had, and the frontend starts once the backend is healthy. New switches for automation: MAZEVAULT_NO_SYSTEMD=1 skips the systemd service and all systemctl calls, and MAZEVAULT_COMPOSE_PROJECT overrides the Compose project name (default mazevault). A newly generated systemd unit now passes the project name (-p) to up and down; the unit is generated only when it does not exist, so an existing /etc/systemd/system/mazevault.service keeps its old command (delete it and re-run the installer to regenerate it).
  • Generated gateway stacks pull the backend from MAZEVAULT_IMAGE_REGISTRY (default the MazeVault Harbor) at the primary's own version, and require MAZEVAULT_GATEWAY_ENVIRONMENT when installed.
  • Customer Helm chart — External Secrets resources use external-secrets.io/v1 (External Secrets Operator 0.16 or newer is required); the backend runs plain HTTP behind the ingress (MAZEVAULT_TLS_ENABLED=false); the Redis access key is URL-encoded in REDIS_URL.
  • Agent deployment API — os_type: darwin and agent_mode: gateway|sidecar are rejected with HTTP 400; supported agents are Linux and Windows hosts.
  • Helm: bffSharedSecret is required, and the ingress must route /api, /swagger and /health to the backend — The new web interface needs a shared secret with the backend. Create it in the Key Vault (openssl rand -base64 48 | tr -d '\n/+=') and set global.keyVault.secretKeys.bffSharedSecret to its name before helm upgrade; while frontend.enabled is true the chart does not render without it. The frontend no longer proxies /api: the chart's ingress sends /api, /swagger and /health to the backend and / to the frontend (container port 3000, Service port stays 80); a self-managed ingress must do the same. Set backend.config.TRUSTED_PROXIES to the pod CIDR of your ingress controller when it is not in a private range.
  • An imported certificate needs its chain — The web interface server verifies the backend through /certs/ca-bundle.crt. import-cert.sh and renew-certs.sh now build that file as the leaf plus the CA chain and warn when no chain was given (the bundle is then the leaf only, and the web interface cannot verify the backend). Re-import with the chain: ./import-cert.sh cert.pem key.pem ca-chain.pem. When the kit starts, an existing ca-bundle.crt is kept and a missing one is seeded from server.crt, also with MAZEVAULT_TLS_SKIP_INIT=true and a certificate placed by hand.
  • Docker Compose: take the new kit — upgrade-mazevault.sh installs the kit's docker-compose.yml after the backup. Run the upgrade script shipped in the new kit (oras pull registry.mazevault.com/mazevault/compose-kit:vX.Y.Z -o mazevault-kit, then sudo bash mazevault-kit/upgrade-mazevault.sh --version vX.Y.Z), or for an offline upgrade pass the new bundle with --offline (its docker-compose.yml is installed too); not the copy in the installation directory. Run with an older kit's docker-compose.yml (no edge service), the script stops before changing anything and names the command. It adds the edge container (see New Features) and recreates the Compose network frontend with the fixed subnet MAZEVAULT_SUBNET (default 172.31.250.0/24), so expect a brief downtime. If docker compose up refuses the changed network, run docker compose down (without -v), then docker compose up -d. If the upgrade stops because the subnet overlaps another Docker network or a host route (the old stack keeps running: "The stack was not touched."), set a free MAZEVAULT_SUBNET in .env first. An unverifiable check only warns "NOT checked" and continues. The script also stops before switching when the installation's server.crt does not name NUXT_CORE_API_SERVERNAME (default MAZEVAULT_DOMAIN), since the web interface could not verify the backend; the message says how to fix it.
  • TRUSTED_PROXIES default in Docker Compose narrows to the kit subnet — An external load balancer in front of the edge, on another private range, must set TRUSTED_PROXIES in .env; otherwise its X-Forwarded-For is ignored and the client IP in the audit log and rate limits becomes the proxy's. The backend and Helm defaults are unchanged (loopback and private ranges).
  • Web interface variables — The frontend container reads NUXT_CORE_API_URL (the backend URL), NUXT_BFF_SECRET (from BFF_SHARED_SECRET), optional NUXT_CORE_API_SERVERNAME (the backend certificate name when it differs from the URL host) and NODE_EXTRA_CA_CERTS (the CA bundle that verifies the backend); see Environment Variables. A custom FRONTEND_PORT with a FRONTEND_URL without that port gets a warning with the exact line NUXT_PUBLIC_ORIGIN=<base>:<port> to add to .env (it is not set automatically: behind a proxy on 443 the port is correctly absent); both installers append the port to FRONTEND_URL and ALLOWED_ORIGINS for a new installation (the offline installer takes FRONTEND_PORT from the environment). DOCS_URL is renamed NUXT_PUBLIC_DOCS_URL; the kit still reads DOCS_URL for this release. An IP address as MAZEVAULT_DOMAIN: the BFF verifies the backend certificate by NUXT_CORE_API_SERVERNAME (default MAZEVAULT_DOMAIN), and an IP address matches only an IP entry of the certificate, so the installers and upgrade-mazevault.sh add NUXT_CORE_API_SERVERNAME=localhost to .env and init-certs adds IP:<domain> to new certificates (see TLS Configuration).

New Features

  • New Web Interface (Nuxt) With a Server-Side BFF — The React application is replaced by a new web interface with a small server (BFF) in the frontend container. The browser holds only the cookie __Host-session (Secure, HttpOnly, SameSite=Strict); no JWT is kept in the browser and the BFF attaches the server-side session to each request. Sign-in: Local, LDAP / AD, Kerberos (the tab is shown only when GET /api/v1/auth/sso/spnego/status reports it available) and the Entra ID SSO button; the Google button is gone because the backend has no route for it. After a sign-in with MFA, one field takes the TOTP or a backup code; too many attempts or an expired pending sign-in return to /login with a notice. A user with must_change_passwd is sent to /change-password and keeps the session. An expired session sends the user to /login?redirect=<page> and back after the next sign-in. Each page checks its permission: without it the page shows "Access Denied" with the missing permission and the sidebar hides the entry. Browser sessions are not an API authentication method for integrations.
  • Light and Dark Mode — The web interface follows the light or dark colour scheme of the operating system, and the switch in the top bar overrides it; the choice is kept in the browser.
  • Encrypted Secret Names After a Local Login — Encrypted secret names unlock after a local password sign-in (not LDAP, Kerberos or SSO). A failed unlock leaves one warning ("Encrypted secret names are unavailable in this session."). The key is held per user in a browser service worker and restored after a reload for at most 2 hours; it is cleared when the session expires. This is client-side encryption of secret names and values of the v2 data with a key derived from the local password, not zero-knowledge storage: the sign-in sends the password to the server.
  • Edge Container — The Compose kit gains the mazevault-edge container (nginx). It terminates TLS on 443 and 80 (80 redirects to HTTPS) and routes /api/, /swagger to the backend and / (the pages and /bff) to the frontend; the frontend no longer terminates TLS. The edge is hardened (no-new-privileges, all capabilities dropped, read-only filesystem, 64 MB by default through EDGE_MEMORY_LIMIT), and the certificate scripts restart it after an import or renewal.
  • OpenAPI 3.1 at /swagger — /swagger serves an OpenAPI 3.1 document (/swagger/doc.json) with Swagger UI 5 (/swagger/index.html). The routes that are not typed in the document (ACME, OCSP, SCEP, CRL, certificate export, the agent websocket, binary downloads, health and Swagger itself; 28 in all) are listed in the document's description. SWAGGER_ENABLED and SWAGGER_HOST apply as before.
  • Long Operations Keep a 300 Second Limit — Through the web interface a request may take 300 seconds (the BFF answers 504 backend timeout after that; the edge proxy_read_timeout and proxy_send_timeout are 300 s towards the backend and the read timeout 310 s towards the web interface, so the BFF's 504 arrives first; the agent websocket 3600 s, uploads up to 50 MB). A failure between the BFF and the backend answers 502, and the BFF logs only the cause code.
  • Added Confirmations — The web interface now asks for confirmation before approving or rejecting a password request, renewing a certificate, approving a CSR on the Discovered Certificates page, generating an ACME EAB key, and exporting a certificate with its private key.
  • Effective Certificate Chain: Intermediate CAs Export With Their Root — Exporting an intermediate CA (or any certificate) that was imported without its issuer chain now includes the issuing CA and root automatically whenever the issuer is present in the organization inventory. A new CertificateChainResolver is the single source of truth for the effective chain: a chain captured on the record at import or issuance is returned byte-for-byte and never extended; only certificates with no stored chain are completed from CA certificates managed in the same organization (including the internal CA and the record's explicit CA link), and a candidate is accepted solely when it cryptographically verifies the certificate's signature — a matching Distinguished Name alone never links two certificates. Roots are included, archived candidates are ignored, a revoked issuer is used only when nothing else verifies and is reported as a warning, and issuers signed with insecure algorithms are rejected with a warning. The same chain is used by the export API (include_chain=true, all formats), by the certificate detail (Chain tab now shows the provenance — Stored with certificate / Resolved from inventory / No chain — plus completeness to the root and warnings), by the agent certificate list, and by the Terraform data source, so every consumer sees identical material. Reading never writes to the database; the chain is persisted only when an import or adoption writes a certificate body that has no chain. The GET /certificates/{id} response gains chain_source, chain_complete and chain_warnings; the mazevault_certificate data source gains chain_pem, chain_source and chain_complete, and its certificate_pem attribute — previously mapped to a field the API never returned — is now populated from public_chain_pem. New Prometheus metric: mazevault_certificate_chain_resolution_total{source,complete}.
  • Discovered Certificates Keep Their Body; Adoption Yields a Deployable Certificate — Agent discovery now persists the public leaf certificate and the issuer chain it observed (migration 000199, columns certificate_pem / chain_pem on discovered_certificates; public material only, never a key). Adopting a discovered certificate therefore produces a managed certificate with a body that can be exported, deployed and verified; managed certificates adopted before this release receive their body automatically on the next agent scan through the existing thumbprint auto-link, and a reported chain is stored as-is (completed from the inventory only when the agent reported none). The discovered-certificate detail (GET /discovered-certificates/{id}) returns certificate_pem, chain_pem and chain_certificates; list responses deliberately omit the body so the Discovered Certificates page stays fast. Identity (serial, thumbprints) is now derived from the certificate DER instead of being trusted from the agent report.
  • Certificate Body From CA Synchronisation and From Manual Import — The body is now also kept for certificates synchronised from a CA listing (HashiCorp Vault, DigiCert, ADCS, SSLMarket and the local-inventory providers used for ACME / I.CA / PostSignum): ParseCertificateMetadata carries the normalized PEM into the discovered record, a re-run of the CA synchronisation back-fills discovered records that were created without it, and a managed certificate linked by thumbprint that has no body receives it the same way the agent path does. In addition, importing the public certificate alone (CRT/PEM/DER, or a PFX) now completes a managed certificate that has no body instead of being rejected as "already exists": the import is matched by thumbprint (or pinned with expected_certificate_id from the certificate detail), the body is stored together with the identity and descriptive columns derived from it (canonical serial, thumbprints, subject/issuer, validity, key and signature algorithms, key usage, SAN set, certificate type; an active status is refreshed, a revoked or pending one is never changed), and a chain-less record is completed from the inventory. Private key handling on such a record follows the approved rule: a key supplied together with the certificate is attached when the record has no custody; a key that matches an existing custody is an idempotent re-import; a key that does not match is rejected with an explicit error after the body was stored — the record is never left without a body because of a wrong key. A locally stored key that could not be verified at the time it was imported (the record had no body) is verified when the body arrives and, if it does not belong to the certificate, is flagged (metadata.private_key_mismatch, audit event certificate.private_key_mismatch) but never deleted. Bulk/file import items report message: "existing certificate record completed with the uploaded certificate body" for this case; the certificate detail shows a Complete certificate action for records without a body, and the discovered-certificate detail explains the manual path. One shared implementation (applyCertificateBodyToManaged) now backs discovery ingest, CA synchronisation, adoption and import, so every path yields the same fully described record (the previous re-scan path set key usage only in memory and did not persist it).
  • Agent File Deployment and Allowlisted Commands — Certificate rotation can now deploy to files on an agent host (Agent File (PEM) target, step agent_file_sync): the agent writes leaf, chain, key and an optional combined bundle (PEM or PKCS#12) according to the target's content mode (public_only, public_chain, full_chain_with_key, key_only), applies permissions and ownership, keeps the previous files as .BAK, installs the chain into the platform trust store when enabled, and runs the optional post-install command. Rotation steps that need a command on the host (service restart, WLST restart script, Oracle orapki) now execute through the new agent agent_command request, which runs only commands matching the agent's built-in allowlist or an operator-configured allowed_commands regular expression. Oracle database wallet targets are now deployed for real: a one-time-password PKCS#12 is staged under <wallet>/mazevault/, imported with orapki wallet import_pkcs12 (auto-login or password-protected wallets via wallet_password_ref), and always removed afterwards; the wallet and PKCS#12 passwords are flagged as sensitive so the agent redacts them in its audit log, its response and any rejection error, and the staging step carries no idempotency key so a retried rotation re-stages the file instead of replaying a stale result. update_tns_names and rac_nodes are refused explicitly rather than ignored. WebLogic targets now deploy through a real weblogic_deploy step: the leaf and key are installed into the custom identity keystore (JKS or PKCS#12, backup via backup_enabled), the issuer chain is optionally imported into the custom trust keystore (trust_store_path + new trust_store_type / trust_store_password_ref; each issuer is staged as its own PEM under <trust-store-dir>/mazevault/, imported with keytool -importcert -trustcacerts, an alias that already exists counts as success and the staged file is always removed), and the managed server or cluster is optionally restarted through the operator's wlst_script_path (restart_mode graceful / force / rolling). The trust-store password is flagged as sensitive so the agent redacts it everywhere, and admin credentials are never placed on a command line — the WLST script must use stored credentials; admin_url, server_name and cluster_name are rejected when they contain shell metacharacters, and a restart mode requires admin_url. Structured agent_command requests (keytool, orapki, WLST script) are executed by the agent directly with their argument vector, without a shell, so a password containing shell metacharacters can neither be reinterpreted nor echoed back by the shell; sensitive values are additionally scrubbed from the command output and error text before they leave the agent. Operator-authored post-install command strings keep their shell semantics.
  • Deployment Targets Are Validated When Saved — Creating or updating a certificate deployment target now validates it against what the system can actually execute, and returns 400 with the reason instead of accepting a configuration that would later fail or be skipped: agent-based targets (agent_keystore, agent_file, database, weblogic) require an agent; a database wallet target requires wallet_path, an Oracle provider (SQL Server, PostgreSQL, MySQL and MongoDB have no wallet — the error names the Agent Keystore / Agent File target to use instead) and update_tns_names=false; a WebLogic target requires keystore_path, a password reference for its trust store and a WLST script for any restart mode; web_server and load_balancer targets are refused with the supported alternative because no deployment mechanism exists for them yet. The workflow builder applies the same validation, so an enabled target that cannot execute fails the rotation with the target ID and reason instead of being skipped silently. Disabling or re-prioritising an existing target with an invalid legacy configuration is always allowed, so operators can take it out of the rotation; enabling it again requires fixing the configuration. The rotation target editor in the UI mirrors these rules: Database Wallet requires an agent and is labelled Oracle, the non-Oracle providers and the Update TNS Names switch are disabled with the guidance text, Web Server is shown as not available yet and Load Balancer is hidden from the picker for new targets (an existing one still opens in edit mode with the explanation, so it can be disabled or deleted — nothing was removed from the API or the code), the backend's validation reason is shown when a save is refused, and the target list gained an Enable / Disable action that changes only the enabled flag — the way to take a legacy target out of the rotation without deleting it.
  • Secret Rotation Backups: Retention, Cleanup, and Manual Recovery — Every secret rotation captures an encrypted pre-rotation backup and now tracks it through an explicit lifecycle (pending → released / restored / recovery_required → acknowledged). A daily cleanup job purges only terminal-state backups past their retention window, while backups whose automatic rollback failed are flagged recovery_required and are never auto-deleted. Operators can list backups, restore or reveal a pre-rotation value, and acknowledge a handled backup through new rotation API endpoints: GET /rotation/backups, POST /rotation/backups/{id}/restore, POST /rotation/backups/{id}/reveal, and POST /rotation/backups/{id}/acknowledge. Reveal and restore additionally enforce secret:read on the secret's environment inside the handler, and for externally-governed (orchestrator-mode / names_only) secrets no local value is ever stored or returned — the operator is directed to retrieve the value from the external provider under their own identity, so the API cannot become a side-channel around Key Vault access control. Retention is configurable via the new MAZEVAULT_ROTATION_BACKUP_RETENTION_DAYS variable (default 7). Two Prometheus metrics were added: mazevault_rotation_backups_purged_total and mazevault_rotation_backups_recovery_required.
  • Rotation Preflight Now Verifies Gateway and Agent Readiness Before Execution — The rotation preflight engine and orchestrator now evaluate the live readiness of every required execution target — gateways and post-action agents — before a rotation is allowed to start, instead of only checking configuration. A new ProbeGatewayByID method probes a specific gateway by ID, and a non-mutating EvaluateReadiness path assesses whether a rotation resource is ready without changing any state. The preflight engine distinguishes required from optional post-action agents, so a rotation that depends on an offline gateway or an unavailable required agent is classified and reported up front (feeding the ready / deferred / blocked / manual-review-required outcomes) rather than failing partway through. The rotation scheduler classifies gate errors more efficiently as part of this work.
  • Service Identity Grace-Period Revoker — A new background job hard-revokes client (service identity) secrets whose grace-period deadline has been reached, completing the "soft revocation" path that RotateClientSecret starts when a grace period is configured. Previously a rotated secret with a grace window relied on the old credential remaining valid, but nothing durably retired it once the window elapsed. The revoker runs periodically from the scheduler (default once per minute), materializes expired grace periods, and writes an audit record for each hard revocation.
  • Agent Lifecycle Management and Deployment Record Handling — Registered agents can now be managed through their full control-plane lifecycle from Deployment → Agents, and agent authentication is lifecycle-aware end to end. A new RequireActiveAgent route guard re-checks the agent's current status in the database on every JWT-authenticated agent request, so an agent that was unregistered, revoked, or deleted can no longer keep calling agent routes with an unexpired token; registration and the legacy hostname-discovery path no longer reactivate an inactive or revoked agent, and runtime consumers (rotation sync-target resolution, SSH key deploy/revoke, keytab rotation) refuse to route work through a non-active agent. Four new agent:admin endpoints back the console: GET /api/v1/agents/:id/lifecycle-impact returns an advisory dependency summary (managed certificates, project bindings, rotation targets, SSH keys, pending keytab work, discovery/config/token references) with best-effort warnings; POST /api/v1/agents/:id/unregister retires an agent — invalidating its mTLS identity and recovery tokens, deactivating project bindings, and stranding pending keytab work — or escalates to a security revoke when the reason category is compromised / lost_credential / stolen_host, requiring force past unresolved blockers; POST /api/v1/agents/:id/reregister retires the old identity and issues a single-use 24-hour replacement bootstrap token; and POST /api/v1/agents/:id/uninstall asks a connected, capable agent to uninstall itself. Remote uninstall is capability-negotiated at request time and supported for Linux packaged (systemd) installs running as root and Windows MSI service installs: the agent accepts first, then a detached copy of the agent binary (escaping the service cgroup via systemd-run on Linux, a DETACHED_PROCESS on Windows) performs the removal and reports the real outcome back over the health channel before self-destructing — on Windows the per-build MSI ProductCode is resolved at runtime from the stable WiX UpgradeCode and removed via msiexec, with an sc delete fallback for non-MSI service layouts. The agent's retirement is finalized only on a reported success; a reported failure keeps the agent active (uninstall_status = failed) for retry, and a missing report is surfaced as awaiting confirmation. Unsupported layouts (non-packaged/manual installs, macOS, offline) return a clear fallback to unregister plus manual removal. Deployment (bootstrap) records gained a soft-delete lifecycle — DELETE /api/v1/deployments/:id (gated by deployment:write, organization-scoped) invalidates the enrollment token and install script without affecting already-registered agents, and the public script/config endpoints then return 404. Deployment list responses use a dedicated token-free DeploymentResponse DTO with a computed usable flag, while the create response returns the bootstrap token exactly once via DeploymentCreatedResponse (the UI, Go SDK, and Terraform provider depend on it). The Deployment → Agents tab becomes an operational console (unregister with impact preview + reason, remote uninstall, re-register with one-time token, visible error states), and Deployment → Deploy gains delete, view-script, and clipboard-fallback actions. Finally, the agent module's own go test suite now runs in PR, dev, and release CI (previously only cross-compiled), and a filesystem-discovery policy-invalidation test that was root-fragile was made deterministic.
  • Rotation Templates Gain Policy Windows and Secret Custody Mode — Rotation configuration templates now carry lead_time_days (days before expiry to start a certificate renewal), grace_period_days (days the previous value stays valid for rollback), and secret_custody_mode (local, transient, or delegated). These fields are exposed through the rotation template API, the Go SDK, and the mazevault_rotation_template Terraform resource, and are aligned end to end with the persisted catalog (migration 000195). The rotation template API schema and its Swagger documentation were updated to describe the new fields. The template additionally exposes project_id (optional project scoping) and retry_delay_seconds, and on the Terraform resource organization_id is now Optional+Computed (server-resolved) with a single shared flatten path so create, read, and update cannot drift; these additions map to already-persisted columns and require no new migration.
  • Discovered Certificates: Search and Assignment Modes — The Discovered Certificates page adds a server-side search filter and explicit assignment modes for adopting discovered certificates into management, including a bulk-adopt endpoint (POST /discovered-certificates/bulk/adopt, gated by discovered_cert:bulk) alongside the single-item adopt path. The standalone Certificate Discovery route was removed and now redirects to Discovered Certificates.
  • Actionable Rotation Status on the Dashboard — The Dashboard now resolves rotation items to their actionable destination through a new navigation resolver, so rotation status entries link directly to the surface where they can be acted on.
  • Deployment Consolidated Under Enterprise Functions (PoC / Enterprise Gated) — Configuration Management, SSH Management, KeyTab Management, and Deployment now live together with Token Management under a single Enterprise Functions navigation group, which is a pure grouping header that only expands/collapses and never navigates on its own. Configuration, SSH, and KeyTab were already gated by their config_management / ssh_management / keytab_management license features; Deployment is now gated the same way through a new deployment_management feature granted to PoC and Enterprise licenses (active, grace, or expiring state). The /api/v1/deployments management group (create / list / delete) is enforced server-side with FeatureLicenseMiddleware, returning HTTP 403 for Small Business or unlicensed tenants, and the Deployment page renders an upgrade banner instead of the tabs; users still see the menu entry when they hold the matching RBAC permission. The unauthenticated, UUID-protected agent bootstrap endpoints (/deployments/:id/script, /config, /agent-binary) are intentionally left ungated so unattended agent installs keep working, and agent/gateway management endpoints are unchanged.
  • Compliance Report PDF Rendered on the Server — New GET /api/v1/compliance/{type}/pdf (permission audit:read; types pci-dss, soc2, gdpr, iso27001, legacy pci and sox) returns the compliance report as a PDF attachment (compliance-report-<framework>-<UTC time>.pdf) with the same content and order as the former in-browser export: summary with score, controls breakdown, certificate and rotation statistics, password policy, recommendations and page numbers. Times are UTC, and the embedded Inter font renders diacritics. The PDF is generated at download time, so its "Generated" time is the download time.
  • Organization Integrations Filter by Category — GET /api/v1/organizations/{id}/integrations takes an optional category query parameter (notification, log_stream, archive; exact match) and then lists only the integrations of that category. Without it the response is unchanged.
  • Report Settings, Report Triggers and Incident Contact Changes Are Audited — PUT /api/v1/reports/settings, POST /api/v1/reports/trigger and PUT /api/v1/organization/incident-contacts now record UPDATE_REPORT_SETTINGS (entity REPORT_SETTINGS, details <n> recipients), TRIGGER_WEEKLY_REPORT (entity WEEKLY_REPORT) and UPDATE_INCIDENT_CONTACTS (entity ORGANIZATION, the organization id, details <n> contacts) with the acting user and IP. The entries never contain e-mail addresses.
  • Governance Report Changes Are Audited — POST /api/v1/governance/reports, POST /api/v1/governance/reports/{id}/analyze and DELETE /api/v1/governance/reports/{id} now record UPLOAD_GOVERNANCE_REPORT (details source_type=<type>, <n> bytes), ANALYZE_GOVERNANCE_DRIFT (details overall_score=<score>) and DELETE_GOVERNANCE_REPORT (entity GOVERNANCE_REPORT, the report id) with the acting user and IP. The entries never contain the report content or its name; refused calls (unknown report) leave no entry.
  • Limit for the Actionable Rotation List — GET /api/v1/dashboard/rotation/actionable accepts an optional limit (an integer of at least 1) that keeps only the first rows of the list; any other value answers 400 Invalid limit. Without it the response is unchanged.

Improvements

  • The Integration List Filters on Enabled — GET /api/v1/integrations accepts an optional enabled (true: only the enabled integrations, false: only the disabled ones); any other value answers 400 invalid enabled. Without it the list is unchanged. The Entra dashboard uses it instead of filtering on the client.
  • Audit Log Pages Through the Server — The console's audit log page now loads one page of 100 entries at a time through the existing limit, offset and total of POST /api/v1/audit-logs/search and shows the total with page navigation; the former page fetched one fixed page of 100 entries and could not reach older ones. The filters and the page are kept in the URL. The API is unchanged.
  • Attach a Private Key to an Existing Certificate (CSR-Issued or Imported Without Key) — Certificates signed from an uploaded CSR are stored with key custody none; operators can now hand MazeVault the key afterwards from Certificate detail → Overview → Import private key (PEM key, encrypted PEM key, PFX/P12, or pasted PEM) with local or secret-manager custody, respecting Orchestrator Mode. No new endpoint was introduced: the dialog reuses POST /certificates/import/file, and the new optional expected_certificate_id form field (also available on POST /certificates/import) pins the request to that single certificate so unrelated material is rejected instead of creating or mutating another record. The multi-file import now pairs standalone .key files with their certificate through the backend preview-multi matching (new key_source_file in the preview response) and sends certificate and key in one request, so a separately uploaded key reaches an already-managed certificate. The duplicate/attach lookup is now SHA-256-thumbprint-first (serial + issuer remains a legacy fallback that refuses rows whose stored thumbprint differs), the custody check honors external custody (an externally stored key is never silently replaced by a local copy), a project-scoped import can no longer mutate a certificate owned by another project, attaching a key never changes the certificate's renewal settings or downgrades a richer stored chain, and the "already exists" error now explains how to add the key.
  • Uploaded CSRs Are Validated Against the Template Before the CA Is Contacted — Uploaded CSRs now fail fast at submission when they violate explicit template policy: the self-signature must verify, the key size must be in allowed_key_sizes when configured (previously enforced only by the internal CA, never by ADCS or external CAs), and the SAN entries must satisfy an explicit san_config (allowed_san_types, san_validation_pattern, max_san_count, and the new require_san flag that rejects CN-only CSRs). Defaults are permissive by design — templates without san_config accept every CSR as before — and system templates (agent/gateway mTLS) are exempt. The requested SAN set and key parameters of an uploaded CSR are now recorded on the certificate request for auditability.
  • ADCS Manager-Approval Requests Are Tracked Instead of Failing — Issuing a new certificate through an ADCS CA that requires manager approval no longer returns HTTP 500 while the CA keeps the enrollment: the request moves to submitted_to_ca and is finalized automatically by the external order poller once the CA approves. CA providers can implement the new optional IssuedCertificateFetcher interface to return the issued leaf for an order ID; ADCS implements it, fixing a latent defect where the poller would have stored the CA chain as the leaf.
  • Certificate Request Lifecycle After Issuance Failures — An auto-approved request whose issuance fails is now marked failed with the error recorded (instead of lingering as approved without a certificate), and a manually approved request whose issuance fails is returned to pending so the approver can retry; a later successful issuance clears the recorded error. The request status enum documents the failed state and the UI shows it.
  • Canonical Certificate Serial Encoding — certificates.serial_number is consistently stored as the decimal big-integer string already used by OCSP, revocation and import lookups. ACME and HashiCorp Vault issuance, external order finalization and the built-in ACME server fast path previously wrote or queried hexadecimal serials, which broke import de-duplication and the ACME server's indexed lookup for those certificates. Vault revocation and status calls translate the canonical serial back to Vault's colon-separated hex format, so both legacy and normalized rows keep working. An idempotent identity backfill (BackfillCertificateIdentity) re-derives serial and thumbprints from the stored DER; it runs in the background on primary startup and is also part of POST /admin/certificates/backfill, which now reports identity_normalized.
  • Template Editor No Longer Flips "Allow Additional SANs" — allow_additional_sans on the certificate-template create/update API is now presence-aware (omitted keeps the stored value; an explicit false is honored). Previously an update that omitted the field — which the template editor UI never sends — reset it to false.
  • Integration Group Scope Resolution for RBAC — A new IntegrationGroupScopeMiddleware resolves a standalone integration-group ID to its owning active project before project RBAC is evaluated, and rejects requests for groups whose project is archived or missing. This lets integration-group endpoints be authorized consistently with the rest of the project RBAC model.
  • SSH Authorized-Key Revocation Lifecycle — Agents can poll for pending authorized-key revocations (GET /agent/authorized-keys/pending-revocations) and confirm completion (POST /agent/authorized-keys/:id/confirm-revocation), and operators can revoke an authorized key (POST .../authorized-keys/:id/revoke) and retry a failed SSH key deployment (PUT /ssh-keys/:id/deploy). A stopped or partially failed rotation execution can be resumed through POST /rotation/executions/:id/resume. The SSH key rotation confirmation message was also clarified.
  • Rotation Runtime Targets Only Route Through Active Agents — When a rotation delegates work to an agent (for example keytab or SSH deployment), the runtime now resolves the linked agent and refuses to route through an agent that is inactive, revoked, or soft-deleted, returning a clear error instead of proxying to a dead target.
  • Rotation Template Numeric Field Validation — Rotation template numeric fields (rotation_interval_days, lead_time_days, grace_period_days, max_retry_attempts, retry_delay_seconds, timeout_minutes) are now validated as non-negative integers across the backend API, the frontend, and the Terraform provider, with structured application errors instead of opaque failures. The retry_delay_seconds field — already persisted and exposed through the API, SDK, and Terraform resource — is now also editable in the rotation-template UI form, so it can be configured and viewed from the console. Bulk certificate adoption surfaces user-friendly messages for partial failures.
  • Graceful Backend Shutdown on SIGTERM — The API server now drains in-flight requests through http.Server.Shutdown when it receives SIGTERM — with a drain deadline kept below the Kubernetes termination grace period — and then stops the log-stream and audit-archive background services before exiting, instead of being force-killed mid-request during rollouts. http.ErrServerClosed is treated as a normal shutdown.
  • SSH Authorized-Key Ownership Enforcement — Listing, deploying, revoking, and removing SSH authorized keys (and listing key pairs) now consistently enforce key-pair ownership — the acting user must own the key pair or hold ssh:admin — matching the ownership checks already applied to get, export, and delete. A non-admin can no longer enumerate another user's key inventory or deploy/revoke/remove keys they do not own, and producer-side revocation and removal, plus the agent's revocation confirmation, now write request-ID-scoped audit records.
  • Rotation Resume Requires an Authenticated Actor — POST /rotation/executions/:id/resume now rejects a request without an authenticated principal with HTTP 401 before the orchestrator runs, and records both successful and failed resumes with full request-ID audit context.
  • Agent Recovery Token Stability (no re-registration lockout) — Agent re-registration no longer rotates a still-valid recovery token on every call. A fresh recovery token is minted only when the agent bootstraps/reinstalls with a deployment token or has no usable token on record; an agent that authenticates with its current recovery token keeps it. Combined with atomic, crash-safe agent-state persistence — the rotated token and identity are staged and committed in memory only after the durable write succeeds, and the temp-file replacement is now atomic on Windows as well (MoveFileEx with replace-existing) rather than only on POSIX — an agent that crashes or fails to persist a rotated token can no longer be permanently locked out. This is a behavioral hardening only — no schema change and no API contract change.
  • Project List and Archive Summary Accept Optional Filters — GET /api/v1/projects/ accepts the optional query parameters search (the project name contains the text, ignoring case) and type (secret, key or certificate, exact match). GET /api/v1/archived/summary accepts the optional archived_from and archived_to (RFC 3339, inclusive, applied to archived_at) and expiring_within_days (an integer of at least 0, keeps items whose days_until_deletion is at most that value); they filter the archived projects, secrets and certificates, never the users, and total_count counts the items listed. A malformed value answers 400. Without the parameters both answers are unchanged, and the console now sends them instead of filtering in the browser.
  • Secret Comparison by Key on the Backend — POST /api/v2/secrets/compare takes the optional query parameter by: blind_index (the default, unchanged) or key, which compares the secrets that have a key (the legacy ones) of the two environments by key and returns each result with a key field (blind_index stays empty), sorted by key. Any other value answers 400. The Secrets tab used to do this comparison in the browser.
  • Certificate Lists Filter, Sort and Page on the Backend — GET /api/v1/certificates/ accepts the optional query parameters search (the common name, primary DNS name, issuer or subject DN, project name, serial number, a SAN DNS name or a tag contains the text, ignoring case), status and type (exact match), expiring_within_days (an integer of at least 0, keeps active certificates that expire within that many days), sort (common_name, primary_dns_name, issuer, expiry_date, days_until_expiry, san_count or project_name) with order (asc, the default, or desc), and limit (at most 500) with offset. GET /api/v1/projects/{id}/certificates accepts search, status (the effective status, including rolling out and deployment failed), type, environment, tag, sort (common_name, serial_number, certificate_type, expiry_date or status), order, limit and offset; environment is also the environment the request is judged in. Filters apply to the certificates the user may read, equal rows are ordered by id so a page boundary does not move, and the X-Total-Count header carries the number of rows the filters keep. A malformed value answers 400. The new GET /api/v1/certificates/analytics returns the dashboard aggregation (totals by status, expiring soon, by type and key algorithm, the top 8 issuers and projects, the expiry timeline of the next 6 UTC months) for the same certificates. Without the parameters the lists are unchanged, and the console now sends them instead of filtering and counting in the browser.
  • Confirmations Before Credential, Deployment and Rotation Actions — The web interface now asks before it reveals a managed token's stored value, exports an SSH private key, downloads a keytab, deploys or removes an SSH authorized key, signs an SSH key, syncs a secret or certificate deployment target now, resumes a rotation execution, creates a service identity (its client secret is shown only once) and deletes a rotation template or a keytab. Every rotation trigger, the system key rotation (confirmed by typing the key's purpose) and the existing delete, renew and unlink confirmations keep their wording. Dry runs are read-only and are not confirmed; creating a token, importing a keytab or an SSH key and saving a rotation config or template stay plain form submits.
  • Page Permission Checks for SSH, Keytabs, Tokens and Service Identities — SSH Management, KeyTab Management, Token Management and Service Identities had no permission check of their own, so a user without access got a 403 message for every request of the page. They now show an access-denied page unless the user holds the permission of their sidebar entry (ssh:read, keytab:read, token:read, service_identity:read); Rotation Templates needs rotation:read and Rotation Problems dashboard:read. Without the licence feature the SSH, keytab and token pages show the upgrade notice and load nothing.
  • User Search on the Backend — GET /api/v1/rbac/users accepts the optional query parameter search: a user matches when the trimmed text is contained in the full name or the email, ignoring case; an empty search changes nothing and no match answers []. The SSH key assignment dialog searches through it instead of filtering the whole user directory in the browser.
  • Configuration Management: Inert Buttons Removed — The new console no longer shows the Preview and Adopt buttons of Configuration Management → Discovered Configurations and the Restore to Pending button of the Staging Area: they did nothing. Ignore (needs config:write, asks for confirmation) is unchanged, and the Staging Area has no Actions column.
  • Billing: Usage Bars Removed — The Usage Limits panel of /billing showed progress bars filled to a fixed 50 % and 20 % that did not reflect any real usage. The new console shows only the limits (Unlimited or Limit: N) for projects and secrets.
  • Billing: Renew License and Activate Need billing:write — /billing needs billing:read, but showed Renew License and the Update License form (Activate) to every user who could open it; renewing and activating need billing:write, so a read-only user's click ended in a 403. Both are now shown only with billing:write; the licence details, the usage limits and the restricted-mode notice stay visible with billing:read.
  • Console: Fewer Needless Requests, Failed Reads Shown as Failures — The gateway Assign Environments dialog showed a failed environment read (for example without env:read) as "No environments found"; it now shows the failure with Retry. Updating and renewing the licence are no longer re-sent after a server error or a lost connection (each call is audited). A certificate or key project no longer reads the project's integrations (only secret projects can show the Entra Apps tab), and the project lists of the console are read only with project:read in some project instead of answering 403.
  • Update Banner for Users With system:read — The new console shows a banner when a newer MazeVault release is available (Update Available, or Critical Update Available for a critical release), with the current and the latest version, the start of the release notes and the download link. It is shown, and the update status is requested, only for users with system:read, the permission the update routes need. Dismiss hides it for the browser tab's session; Re-check asks the update server again and shows an error message when that fails.
  • Confirmations Before Deployment, Gateway and Entra Actions — The web interface now also asks before it generates a deployment (it creates a bootstrap token), re-registers an agent (a replacement token), requests a remote uninstall of an agent, saves the agent version policy (it applies to every deployed agent), saves a gateway's environment assignment (gateway tasks are routed by it), adds a credential to an Entra app (it creates the secret in Entra ID), synchronises an Entra integration (Sync from Entra rewrites the stored applications and credentials) and applies an uploaded system certificate. Deleting an Entra credential keeps its wording; unregistering an agent keeps its dialog with the reason and the impact, and changing one agent's update channel is not confirmed.

Bug Fixes

  • Linux Agent Could Not Run on Hardened or RHEL 8 Hosts — Two independent defects stopped the Linux agent on customer hosts. (1) The release agent binary was built with cgo enabled, so it was dynamically linked against the build machine's glibc and failed on RHEL/Rocky/AlmaLinux 8 with /lib64/libc.so.6: version `GLIBC_2.34' not found; the agent is now built with CGO_ENABLED=0 -trimpath and is statically linked, running on any glibc. (2) The deployment install script placed the binary under /var/lib/mazevault/bin; on hosts that mount /var noexec (CIS/STIG baselines) systemd failed with status=203/EXEC in an endless restart loop. The installer and the rpm/deb package now install the binary to /opt/mazevault-agent/bin (owned by the agent user, listed in ReadWritePaths so self-update keeps working, SELinux bin_t). The installer was reworked for hardened hosts: set -euo pipefail, preflight (noexec check of the binary directory via findmnt, SELinux detection, required tools), no curl -k fallback any more (with a MazeVault CA the download uses it and falls back to the system trust store, never to insecure), the HTTP status and the server's error body are printed when the download fails (e.g. 503 agent binary downloads not configured), a curl failure after HTTP 200 (truncated transfer) counts as a failed download, and the signed binary is staged as mazevault-agent.new and executed as mazevault-agent (version) before it replaces the installed binary and before the unit is written, so a noexec mount, an SELinux denial or a glibc mismatch stops the installer with a clear message, leaves a previously installed agent untouched and never causes a restart loop, agent.yaml is written 0640, systemctl restart is used so a re-run updates a running agent, hosts installed with the previous layout are migrated (old binary removed, ExecStart drop-in overrides removed, other drop-ins kept), and exit codes are 2 preflight / 3 download or verification / 4 service not running (status and journal printed with tokens and UUIDs redacted). When the server has no CA configured, the installer no longer writes an empty ca.crt and the generated agent.yaml leaves server_ca_file empty, so the agent validates the server with the system trust store; the Windows installer likewise skips writing and importing an empty CA. The agent's self-update and the remote-uninstall runner now choose a directory that is writable and not on a noexec mount (binary directory, state directory, then the temp directory) instead of assuming /tmp is executable, and report the rejected directories when none qualifies. Remote uninstall recognises script-installed hosts (no rpm/deb package): the unit, drop-ins and the agent directories are removed directly instead of a failing rpm -e, and stopping the service, removing the unit file and removing /opt/mazevault-agent are required steps whose failure is reported to the server instead of a false success, /opt/mazevault-agent is removed only when the binary actually lives there, and the root requirement is explained in the error. New optional variable MAZEVAULT_CA_FILE (release kit, installers, check-config.sh, Helm values, documentation) names the CA embedded into install scripts; the Compose kit mounts CUSTOM_CERT_DIR into the backend as /custom-certs for it. Operators who worked around 203/EXEC with a drop-in pointing to /usr/local/bin only need to re-run the install script.
  • A Backend Outage No Longer Signs the User Out — When the session check of the web UI fails with a server error, a gateway error or a network error, the UI now keeps the user signed in and the vault unlocked (a backend that does not answer within 15 seconds counts as such an error); on first load it shows "MazeVault is not reachable. Try again in a moment.", and "Back to home" checks again. Only an expired or missing session (HTTP 401) leads to the login page. A refused call of a background check (such as the encryption status after sign-in) shows its own single message instead of two.
  • The Wrong-Old-Password Message No Longer Carries Internal Text — Changing a password with a wrong current password (POST /api/v1/auth/password) still answers HTTP 400, but the message is now only UNAUTHORIZED: invalid old password; the internal text of the password hash comparison no longer appears in the response.
  • A Staged Entra Secondary Is Created Once — Two concurrent POST /api/v1/entra/credentials/{id}/staged-secondary calls (a double click, a retrying client, or calls for two credentials of the same application) both passed the "one staged secondary per application" check and each created a live client secret in Entra and a pending secondary. An application now has one staged cycle at a time: the call claims its credential while the application is locked, before it asks Microsoft Graph (or the gateway) for a secret, and answers 409 without touching Entra while another credential of the application is rotating or a staged secondary is pending (a staged secondary credential is already in progress for this application; …, a credential of this application is rotating; …). A staged secondary or a retiring former primary cannot start a staged cycle (409), and a credential whose normal rotation awaits verification (status pending_verification) counts as rotating. Deleting the application waits for a running claim instead of failing on a database deadlock. The claim is released when the call ends, also when it fails; if the server stops mid-call, the stuck-rotation watchdog releases it after 30 minutes.
  • Entra Credential Changes Are Audited — Deleting an Entra credential (DELETE /api/v1/entra/credentials/{id}) left no audit entry, saving its rotation configuration (POST …/rotation-config) only one without the user, and the dry run of PUT …/rotation-config none. They now record DELETE_ENTRA_CREDENTIAL, ENTRA_ROTATION_CONFIGURED (with the enabled flag, the lead and grace days and the number of post-rotation actions) and ENTRA_DRY_RUN_PREFLIGHT with the user, after the change succeeded, one entry per save (the additional entry without a user, entra.rotation.configured / entra.rotation.disabled, is no longer written); a refused call records nothing.
  • Gateway Activation, Environment Assignment, DR Failover and Bootstrap Tokens Are Audited — Activating or deactivating a gateway, assigning its environments, the DR activation and the failback, and minting a gateway or agent bootstrap token left no audit entry. They now record ACTIVATE_GATEWAY / DEACTIVATE_GATEWAY and UPDATE_GATEWAY_ENVIRONMENTS (entity GATEWAY, the gateway id; the environments and their modes), ACTIVATE_GATEWAY_DR / FAILBACK_GATEWAY_DR (entity ENVIRONMENT, the environment), CREATE_GATEWAY_BOOTSTRAP (entity GATEWAY, the new gateway id and its name) and CREATE_AGENT_BOOTSTRAP_TOKEN (entity DEPLOYMENT) with the user and the IP address. No entry carries a token; a refused call records none.
  • A Staged Entra Secondary Is Verified and Promoted Once — Concurrent POST /api/v1/entra/credentials/{id}/verify-secondary or POST …/promote-primary calls (a double click, a retrying client) could all succeed: a secondary was promoted twice and its former primary demoted twice with its rotation count raised for each call. The transition now claims the credential in its current phase first: one call succeeds, the others answer 409 and change nothing.
  • The Entra Credential Routes No Longer Show Internal Error Text — POST /api/v1/entra/credentials/{id}/staged-secondary and POST …/promote-primary answered every failure with 409 and its text, including a Microsoft Graph or database failure and a configuration that could not be decrypted (credential not found: decryption failed: …); PUT …/rotation-config, POST …/dry-run and GET /api/v1/entra/subjects/{id} put the internal error text into their 500. A refused state transition still answers 409 with its message (a staged secondary already in progress, a credential that is the primary, not eligible or still soaking); any other failure now answers 500 {"error": "Internal server error"} and the cause is in the server log.
  • The Entra Dry Run Shows No Internal Error Text — A failed check of an Entra credential's dry run (POST /api/v1/entra/credentials/{id}/dry-run, PUT …/rotation-config with dry_run) carried the internal error in checks and in the stored dry_run_last_result, for example a decryption error of the integration configuration. A failed check now carries a fixed reason (credential not found or not readable, app registration not accessible, integration not found, integration configuration is invalid, cannot connect to Entra ID); the cause is written to the server log.
  • Saving an Entra Rotation Configuration as a Dry Run Stores Nothing — POST /api/v1/entra/credentials/{id}/rotation-config (the Configure Rotation dialog) ignored dry_run and saved the configuration, although the dialog's Dry Run Mode promises preflight checks only. Like the PUT, it now answers {"dry_run": true, "preflight": {…}} with the read-only preflight, stores nothing and records ENTRA_DRY_RUN_PREFLIGHT. An API client that sent dry_run: true to have the configuration saved must drop the flag.
  • The Entra Rotation Dialog Shows the Dry Run Result — With Dry Run Mode on, the Configure Rotation dialog of an Entra credential closed as if the configuration had been saved. Its button now reads Run Dry Run; the dialog stays open, shows the preflight result (status, message and each check) and stores nothing. The switch counts only while automatic rotation is enabled, where it is shown.
  • Updating an Entra Rotation Config Keeps the Post-Rotation Actions It Sends — PUT /api/v1/entra/credentials/{id}/rotation-config (used by the Terraform provider's mazevault_entra_rotation_config on every update) ignored the post_rotation_actions of its body: it replaced them with actions derived from the legacy fields, and once advanced actions were configured it refused every update. The sent actions are now stored (an empty list clears them); without the field the legacy fields apply as before.
  • Invalid Entra Rotation Settings Answer 400 — POST /api/v1/entra/credentials/{id}/rotation-config (the Entra rotation dialog) answered an invalid post-rotation action (an unsupported type, a missing field, an unsupported on_failure) and an enabled rotation that sent no post_rotation_actions while advanced ones are configured with 500 internal error, and PUT …/rotation-config (the Terraform provider) answered these and a soak_window_hours below 1 with 500 and the message. They now answer 400 with the message (for example post_rotation_actions[0]: unsupported Entra post-rotation action type "ftp_upload"); nothing is saved.
  • Entra App Changes Are Audited — Creating, updating and deleting an Entra app registration, adding a credential to it, marking it managed by MazeVault and syncing an integration's apps from Entra changed MazeVault and Entra without an audit entry. They now record CREATE_ENTRA_APP, UPDATE_ENTRA_APP (the changed fields), DELETE_ENTRA_APP, ADD_ENTRA_CREDENTIAL (type and display name; never the secret), MARK_ENTRA_APP_MANAGED and SYNC_ENTRA_APPS after the change; a refused or failed call, and marking an app that is already managed, records nothing.
  • Adding a Certificate Credential to an Entra App Answers 400 — POST /api/v1/entra/apps/{id}/credentials (the Add Credential dialog of an Entra app, which offers Certificate) answered a credential_type other than client_secret with 500 internal error, since only client secrets can be created there. It now answers 400 unsupported credential type "certificate": only client_secret credentials can be added before anything is read or sent to Entra.
  • The Entra App Read No Longer Shows Internal Error Text — GET /api/v1/entra/apps/{id} answered every failure with 404 and the error text, including a database failure or an integration configuration that could not be decrypted (decryption failed: …), and GET /api/v1/entra/{integrationId}/subjects put the database text into its 500. A missing app still answers 404 record not found; any other failure now answers 500 without internal text (the read's {"code": "INTERNAL_ERROR", "message": "internal error"}, the subjects' {"error": "Internal server error"}) and the cause goes to the server log.
  • A sync interval below one minute is saved as one minute — In Project Settings → Sync Settings and in a project's Settings → Sync Configuration, an interval below 1 minute (for example a negative number) was stored as typed. It is now saved as 1 minute, matching the "minimum 1 minute" help text. An empty field or 0 still falls back to 5 minutes.
  • Editing a default config template keeps its Default flag; a refused save keeps the wizard open — In Project Settings → Config Templates, saving an edit of a template marked Default no longer clears the Default flag (the wizard sent is_default: false), and a save the backend refuses now shows its message inside the wizard instead of closing it.
  • Suggest Rules in the Secret Naming Policy editor adds the suggested rules — In Project Settings → Config Templates, the Secret Naming Policy editor's Suggest Rules button read a response field the backend never sends (suggested_rules instead of suggestions), so it never added a rule. It now adds the suggested rules the policy does not have yet, as enabled warnings, and shows the backend's message when the request fails. The Exact Values, Project Scope, Environment Scope and Subscription Scope fields of the Resource Visibility Policy editor accept a trailing comma while typing, so a second value can be entered.
  • Failover Preconditions Answer 409, an Invalid Environment 400 — POST /api/v1/admin/failover/{environment}/activate-dr and …/failback answered 500 when the failover could not run as asked: no DR standby gateway, a primary that is still active, or no primary gateway. These routes are the documented DR operator API, and a state the operator must change first is not a server error. They now answer 409 with the same message (for example primary gateway … is still active for environment NPR; deactivate primary first to prevent split-brain), an invalid environment answers 400, and a database failure answers 500 Internal server error.
  • The Gateway Routes No Longer Show Internal Error Text — The gateway admin routes (/api/v1/admin/gateways…, GET /api/v1/admin/failover/{environment}, GET /api/v1/admin/gateway-health), the gateway-facing task routes (/api/v1/gateway/tasks…, GET /api/v1/gateways/{id}/tasks), the gateway CSR (POST /api/v1/gateways/csr, which answered Failed to submit CSR: …) and GET /api/v1/gateways/{id}/health put the text of the failing service or database into their 500. They now answer {"error": "Internal server error"} and the cause goes to the server log.
  • The Agent Routes No Longer Show Internal Error Text — The agent lifecycle routes (GET /api/v1/agents/{id}/lifecycle-impact, POST /api/v1/agents/{id}/unregister, POST /api/v1/agents/{id}/reregister, POST /api/v1/agents/{id}/uninstall) and the agent password requests (POST and GET /api/v1/agents/me/password-requests) put the text of the failing service or database into their 500, for example agent has no organization; cannot issue a replacement token. They now answer {"error": "Internal server error"} and the cause goes to the server log.
  • Ignoring and Adopting a Config Discovery Result Works Again — POST /api/v1/discovery-results/{id}/ignore (the Ignore action of Configuration Management → Discovered) and POST /api/v1/discovery-results/{id}/adopt answered 500 with an empty body for every call: the handlers read the user id in the wrong form. They now ignore or adopt the result, answer {"status": "ignored"} or the new configuration file, and write the IGNORE_CONFIG_DISCOVERY_RESULT and ADOPT_CONFIG_DISCOVERY_RESULT audit entries.
  • Unknown Configuration Files and Discovery Results Answer 404, an Adopted Result 409 — GET /api/v1/config-files/{id} and POST /api/v1/discovery-results/{id}/adopt answered an unknown id with 500 Internal server error, and POST /api/v1/discovery-results/{id}/ignore answered it 200 {"status": "ignored"} without changing anything. They now answer 404 (configuration file not found, discovery result not found), and adopting a result that is already adopted answers 409 discovery result already adopted instead of 500.
  • The Deployment Routes No Longer Show Internal Error Text — POST, GET and DELETE /api/v1/deployments… and the anonymous install routes GET /api/v1/deployments/{id}/script and …/config put the text of the failing service or database into their 500 (for example no trusted agent signing keys: install scripts cannot verify the agent binary). They now answer {"error": "Internal server error"} and the cause goes to the server log; the 400, 404, 410 and 422 answers are unchanged.
  • Security: Adopting a Config Discovery Result Is Judged in the Target Project — POST /api/v1/discovery-results/{id}/adopt created the configuration file in the project_id of the body, and with extract_secrets stored the file's secrets there, without checking that project: any holder of config:write could write into any project, also one of another organisation or one that does not exist. The caller now needs config:write in that project (a role scoped to the project may adopt into it, and only into it) and, to extract secrets, secret:write in the project's dev environment, where the extracted secrets are stored. A project the caller may not write answers 403; an unknown project, or one of another organisation, answers 404 Project not found.
  • The Naming Convention Routes Answer Unknown Templates With 404 and Hide Internal Errors — POST /api/v1/naming-conventions/{id}/render, …/parse, …/provision, …/terraform and …/cross-validate answered 500 with the text of the failing service or database (for example naming convention template not found: record not found). An unknown template now answers 404 {"error": "naming convention template not found"}, a resource type without a pattern in the template or a resource name without a project code 400 with that message, and any other failure {"error": "Internal server error"} with the cause in the server log.
  • The Sync Event Lists No Longer Show Internal Error Text — GET /api/v1/sync/events (the replication peer) and GET /api/v1/sync-dashboard/events put the text of the failing database into their 500. They now answer {"error": "Internal server error"} and the cause goes to the server log.
  • Security: Naming Conventions Are Judged in Their Organisation, Config Templates of an Unknown Organisation Answer 404 — POST /api/v1/naming-conventions/{id}/render, …/parse, …/provision, …/terraform and …/cross-validate read a template of any organisation, and …/provision created a project in the organization_id of the body without checking it: a holder of config:read or config:write limited to one organisation could read another organisation's naming conventions and create projects in another organisation. The template's organisation, and for provisioning the body's organisation, now need the permission there (403 otherwise); an unknown organisation in the body answers 404 Organization not found. The config template routes …/organizations/{id}/config-templates… now also answer 404 Organization not found for an unknown organisation (the list answered an empty list).
  • Config Template Changes and Provisioned Projects Are Audited — Creating, updating and deleting a config template (…/organizations/{id}/config-templates…) left no audit entry, nor did a project created by POST /api/v1/naming-conventions/{id}/provision. They now write CREATE_CONFIG_TEMPLATE, UPDATE_CONFIG_TEMPLATE (with the fields the update applied; an update that applies none records nothing) and DELETE_CONFIG_TEMPLATE entries, and CREATE_PROJECT naming the naming convention and resource the project was provisioned from.
  • Creating a Config Template Ignores Server Fields in the Body — POST /api/v1/organizations/{id}/config-templates took id, created_at and updated_at from the request body, so a client could choose the new template's id (and collide with an existing one) or back-date it. The server now sets them itself; organization_id was already taken from the path. Updates already accepted only the template's own fields.
  • Security: The Config Template Preview Is Judged in Its Organisation — POST /api/v1/organizations/{id}/config-templates/preview judged only org:admin in the organisation of its path, unlike the other config template routes. It now first requires config:write in that organisation (403 otherwise), and an unknown organisation answers 404 {"error": "Organization not found"} (before: 403 from the org:admin check, or 404 Project not found).
  • Security: Naming Rule Suggestions Are Judged in Their Organisation — GET /api/v1/organizations/{id}/secret-naming-policy/suggest analyses the secret keys of the organisation of its path. It now requires config:read in that organisation in the handler as well (403 otherwise), and an unknown organisation answers 404 {"error": "Organization not found"} instead of 200 with no suggestions.
  • The Config Template Preview No Longer Returns the Text of a Failed Azure Call — When Microsoft Graph or Azure Resource Manager refused the call of POST /api/v1/organizations/{id}/config-templates/preview, the 502 sent the error text of that call, including the body Azure returned, in details. It now sends a fixed details (Microsoft Graph API request failed or Azure API request failed), like the Azure discovery routes, and the cause goes to the server log with the request ID.
  • Unknown Sync Rules, Conflicts and Projects Answer 404 — PUT and DELETE /api/v1/projects/sync-rules/{ruleId}, GET …/{ruleId}/conflicts, POST …/{ruleId}/sync, POST /api/v1/projects/sync-conflicts/{conflictId}/resolve and GET, PUT and DELETE /api/v1/projects/{id}/sync-settings answered an id that does not exist with 500 (record not found, Failed to get project, …), 200 [], 202 Sync triggered or 204 without changing anything. They now answer 404 with {"error": "Sync rule not found"}, "Sync conflict not found" or "Project not found".
  • A Sync Rule Without a Valid Target Environment Answers 400 — POST /api/v1/projects/{id}/sync-rules answered a pull or bidirectional rule without target_environment (target_environment is required for pull sync rules) and a target_environment that is not an environment identifier (invalid target_environment "…": …) with 500. Both are input the caller can correct and now answer 400 with the same message.
  • The Sync Rule Routes No Longer Show Internal Error Text — GET and POST /api/v1/projects/{id}/sync-rules, PUT and DELETE /api/v1/projects/sync-rules/{ruleId}, GET …/{ruleId}/conflicts and POST /api/v1/projects/sync-conflicts/{conflictId}/resolve put the text of the failing database or provider call into their 500 (for example ERROR: invalid input syntax for type boolean for a value of the wrong type). They now answer {"error": "Internal server error"} and the cause goes to the server log; the 400 messages of refused input are unchanged.
  • Creating a Sync Rule Ignores Server Fields in the Body — POST /api/v1/projects/{id}/sync-rules kept created_at, updated_at, last_sync_at and last_sync_status from the body, so a client could back-date a rule or give it a forged failed sync that the Synchronization Problems page then listed. The create now sets them itself (the current time, no last sync); the id was already generated by the server.
  • Security: The Organisation Sync Settings Are Judged in Their Organisation — GET and PUT /api/v1/organizations/{id}/sync-settings now judge the organisation of their path like the other organisation routes: org:read (org:admin for the update) must hold in that organisation (403 otherwise), and an unknown organisation answers 404 {"error": "Organization not found"}; before, a read answered 500 Failed to get organization and an update 200 Sync settings updated without changing anything.
  • Security: Sync Failure and Blocked-Seed Acknowledgements Are Judged Where They Belong — POST /api/v1/dashboard/sync/failures/{ruleId}/acknowledge and POST /api/v1/dashboard/sync/blocked-seeds/{incidentId}/acknowledge were judged only in the environment of the request: any holder of sync:write could acknowledge every project's failures and blocked seeds, also in environments their role does not cover, and a role limited to a project could acknowledge none. They are now judged in the project and target environment of the rule, or in the organisation, project and environment of the incident (403 otherwise), so a project-scoped role acknowledges its own project's; an unknown rule or incident answers 404 (Sync rule not found, Incident not found; before: 500 Failed to acknowledge sync failure, 400 Failed to acknowledge blocked seed).
  • Security: Sync Rules Are Judged in Their Target Environment — PUT and DELETE /api/v1/projects/sync-rules/{ruleId}, GET …/{ruleId}/conflicts, POST …/{ruleId}/sync and POST /api/v1/projects/sync-conflicts/{conflictId}/resolve were judged in the rule's project but in the environment of the request, so a role limited to dev could change, delete, run or resolve the conflicts of a rule that syncs into prod, and could retarget a dev rule to prod. They are now also judged in the rule's target environment; an update that sets target_environment is judged in the new value as well (an empty value means every environment), and POST /api/v1/projects/{id}/sync-rules in the environment the new rule targets (403 insufficient permissions for environment otherwise, nothing changed). A rule without a target environment (a push rule) needs a role not limited to some environments.
  • Security: A Sync Rule Is Judged in Its Integration's Environment — POST /api/v1/projects/{id}/sync-rules, and PUT /api/v1/projects/sync-rules/{ruleId} when it changes integration_id, did not judge the integration the rule syncs through: a role limited to dev could create a dev rule over a prod integration, and a pull rule then reads the prod store's secret values into dev. They now also require integration:read in the integration's environment (403 insufficient permissions for environment otherwise, nothing changed).
  • Security: Every Sync Rule Change and Run Is Judged in Its Integration's Environment — An existing dev rule over a prod integration (made by a role allowed in prod) was judged only in its target environment when it was updated, deleted or triggered, or when one of its conflicts was resolved. A role limited to dev could switch it to push and so write dev values into the prod store. PUT and DELETE /api/v1/projects/sync-rules/{ruleId}, POST …/sync-rules/{ruleId}/sync and POST /api/v1/projects/sync-conflicts/{conflictId}/resolve now also require integration:read in the environment of the rule's integration (403 otherwise, nothing changed).
  • Sync Settings and Sync Rule Changes Are Audited — PUT /api/v1/organizations/{id}/sync-settings, PUT and DELETE /api/v1/projects/{id}/sync-settings, POST /api/v1/projects/{id}/sync-rules, PUT and DELETE /api/v1/projects/sync-rules/{ruleId} and POST …/{ruleId}/sync left no audit entry. They now record UPDATE_ORG_SYNC_SETTINGS (entity ORGANIZATION, the new values), UPDATE_PROJECT_SYNC_SETTINGS (entity PROJECT, the changed fields), RESET_PROJECT_SYNC_SETTINGS, CREATE_SYNC_RULE (entity SYNC_RULE, name, direction and target environment), UPDATE_SYNC_RULE (the fields it applies; none, no entry), DELETE_SYNC_RULE (the name) and TRIGGER_SYNC_RULE; a refused call records nothing.
  • Security: The Deployment List and Delete Judge the Organisation — GET /api/v1/deployments?organization_id= and DELETE /api/v1/deployments/{id}?organization_id= now judge the named organisation like the other organisation routes: another organisation than the caller's needs the deployment permission there (403 insufficient permissions for organization otherwise), and an unknown organisation answers 404 Organization not found instead of an empty list or 404 deployment not found.
  • Security: The Deployment Create Judges the Organisation — POST /api/v1/deployments/ now judges the body's organization_id like the list and the delete: another organisation than the caller's needs deployment:write there (403), and an unknown organisation answers 404 Organization not found instead of 500. The deployment's bootstrap token enrols agents into that organisation, so it can no longer be minted for an organisation the caller may not manage.
  • Security: A Resource Visibility Policy Cannot Be Converted Without org:admin — PUT /api/v1/organizations/{id}/config-templates/{templateId} checked org:admin only for the type a template would have after the update. A config:write holder without org:admin could therefore send {"template_type": "parser", …} for an existing resource visibility policy and so turn off the organisation's visibility rules. The update now needs org:admin when the existing template or the new type is a visibility policy (403 otherwise, nothing changes), as the delete already did.
  • The Organisation Licence Answers 404 for an Unknown Organisation — GET /api/v1/organizations/{id}/license answered an unknown organisation with 500 (or, when a backend licence was installed, with that licence), and PUT validated the key and answered as if it had been stored. Both now answer 404 Organization not found, and both judge the organisation in the path: a role limited to another organisation answers 403.
  • Unknown Gateways Answer 404; Licence and Agent Policy Failures Show No Internal Text — POST /api/v1/admin/gateways/{id}/activate and …/deactivate answered an unknown gateway with 500; they now answer 404 gateway <id> not found, as the gateway read does. PUT /api/v1/organizations/{id}/license answered a failure to store the key with the database's error text as 400; an invalid key still answers 400 with its reason, a store failure now a generic 500. The 500 of POST /api/v1/entra/credentials/{id}/verify-secondary, of the agent version policy save and read and of the agent update channel now answer Internal server error and write their cause to the server log.
  • System Certificate Uploads and Licence Changes Are Audited — Uploading the system TLS certificate (POST /api/v1/system/certificate), updating an organisation's licence key (PUT /api/v1/organizations/{id}/license) and requesting a licence renewal (POST /api/v1/license/renew) changed the system without an audit entry. They now write system_certificate.uploaded (the certificate's serial number, subject and expiry), license.key_updated (the licence type; the key itself is never logged) and license.renewal_requested (the resulting status).
  • The System, Licence and Health Routes No Longer Show Internal Error Text; a Certificate That Does Not Match Its Key Is a 400 — GET /api/v1/health/database answered a failing health check with the error text in its 503 (it now answers {"error": "Database health check failed"}), and GET /api/v1/license/status, POST /api/v1/license/renew, GET /api/v1/organizations/{id}/license and GET /api/v1/system/updates/check put the text of the failing service or database into their 500; they now answer Internal server error and the cause goes to the server log. Uploading a system certificate (POST /api/v1/system/certificate) whose private key does not belong to the certificate answered 500; it now answers 400 with the same message (Failed to apply certificate: invalid certificate/key pair: …), and a failure to store or write the certificate answers 500 Internal server error.
  • The SSH Rotation Interval and the SSH Rotation Schedule Save — The SSH rotation dialog sent its requests to /api/ssh/rotation…, which does not exist, so an SSH rotation schedule could not be created or changed, and the interval edit of the SSH Rotation tab called PUT /ssh/rotation-configs/{keyPairId}, which does not exist either. The dialog now uses /api/v1/ssh/rotation, and the interval is saved with PUT /api/v1/ssh/rotation/{id} (interval_days, status) when the key pair has a rotation config, otherwise with POST /api/v1/ssh/rotation (key_pair_id, interval_days).
  • The Access Control Identities Tab Creates Working Service Identities — The Identities tab of Access Control sent name, client_id and role, which the backend always rejected (display_name and owner_email are required), and listed fields a service identity does not have. The tab now shows the same list and form as the Service Identities page and needs service_identity:read.
  • Saving a Secret's Rotation Config Is Audited — PUT /api/v1/secrets/{id}/rotation-config changed the rotation of a secret (enabled, interval, schedule, notification emails, target environment) without leaving an audit entry. It now writes rotation_config_updated with the new settings; the update of an agent integration records the settings it saved.
  • The Secret Rotation Routes No Longer Answer Database Errors, and Unknown Objects Are 404 — The rotation config, links, deployment targets and the project integration list answered a failing database with its error text, and an unknown secret, an unknown integration or an unknown agent integration with a 500 (PUT /secrets/{id}/rotation-config and POST /secrets/{id}/links and /rotation/targets failed on a foreign key and returned its SQL text; PUT /agent-integrations/{id} answered 500). They now answer the fixed "Internal server error" body, 404 for an unknown secret, integration or agent integration, and 409 when a secret is linked to the same integration twice.
  • An Invalid Environment on an Agent Integration Update Is a 400 — PUT /api/v1/agent-integrations/{id} with an environment that is not a valid slug answered 500 "An unexpected error occurred". It now answers 400 with the reason.
  • ADCS Accounts Created in the Web Interface Can Enroll — The web interface stores the enrollment URL, enrollment mode, CA config string, agent, service URL, Verify SSL and timeout of a Microsoft ADCS account with its credentials, while the ADCS provider read them only from the account settings, so such an account enrolled without an enrollment URL and every issuance, CA certificate fetch and template sync failed. The provider now takes these values from the credentials wherever the settings lack them (a value in the settings still wins); existing accounts work without being edited.
  • Rotation Config Updates and Failed Backup Restores or System Key Rotations Are Audited — PUT /rotation/configs/{id} changed the rotation schedule, interval and environment of a secret without an audit entry. It now writes rotation_config_updated with what was set. A rotation backup restore that fails after it started writes ROTATION_BACKUP_RESTORE with outcome=failed, and a system key rotation that fails after the publish gate let it through writes ROTATE_ENCRYPTION_KEY with outcome=failed (type and purpose, never a key). The fields a request may set on a rotation config are an explicit allowlist (enabled, interval, target environment).
  • The Rotation Routes Answer Unknown Objects with 404 and Never Show Database Errors — POST /rotation/configs/{id}/preflight for an unknown config answered 500 record not found, POST /rotation/configs for an unknown secret failed on a foreign key (500 with the database text), GET /rotation/executions/{id} answered 404 for any failure including a broken database, and the rotation, backup and system key routes put the text of the failing service or database into their 500 answers. They answer 404 for an unknown config, secret or backup, and every 500 carries only Internal server error (the cause is in the server log). POST /admin/rotation/rotate with a key type the server does not know answers 400 unsupported key type instead of a 500.
  • The Rotation Triggers Never Show Database or Service Errors — A failing rotation trigger (/rotation/configs/{id}/trigger, /rotation/resources/{kind}/{id}/trigger, /secrets/{id}/rotate, /certificates/{id}/rotation/trigger) put the text of the failing service, provider or database into its 500 answer (for example Failed to trigger rotation: …, Sync failed: …). Every 500 now carries only Internal server error (the cause is in the server log). A deployment target that cannot be read answers 500 instead of 404, and an unknown Entra credential answers 404 Entra credential not found instead of the database text.
  • A Failed Rotation Trigger Is Audited — A manual rotation trigger whose rotation started and then failed (500) left no audit entry, although the provider, agent or gateway may already have acted. It now writes its usual entry (rotation_triggered, or rotation_deployment_sync_triggered for a deployment target) with outcome=failed and without the error text. A trigger the publish gate refuses, or one refused as unknown or conflicting, is not audited as a trigger.
  • A Resumed Rotation Survives a Closed Browser Tab — POST /rotation/executions/{id}/resume ran the remaining steps on the request's connection: when the client went away (a closed tab, a proxy timeout) the steps stopped mid-rotation and the execution lock could not be released, so every further resume answered 409 "already in progress" for up to 10 minutes. The resume now runs to its end and releases its lock whatever happens to the connection.
  • The Rotation Executions List Shows the Organization's Latest Executions — GET /rotation/executions read the 100 newest executions of the whole installation and then kept those of the caller's organization, so an organization whose executions were not among the newest 100 overall saw a short or empty list. The organization is now filtered before the limit: the list holds the organization's 100 newest executions (and the readable environments of them).
  • Signing an SSH Key Without a User Answers 401 — POST /api/v1/ssh/sign for a principal that carries no user answered 500 (the certificate's issuer is a foreign key to users). It now answers 401 authentication required, like create-and-deploy.
  • SSH Private Key Export Returns PEM, and SSH Key Rotation Keeps the Key Type — Exporting an RSA key pair made by create-and-deploy returned raw binary instead of a PEM private key; every export now returns PEM (older stored keys are converted on read, new ones are stored as PEM). Rotating an SSH key pair always generated an RSA 4096 key; it now keeps the key pair's type (RSA, Ed25519 or ECDSA) and size.
  • Rotation Templates Work With API Tokens — The rotation template routes (/api/v1/rotation/templates) read the organization only from a UI session, so every API token (Terraform, CLI, SDK) was refused with 403 record not found. A token now works in its own organization, like a session, and another organization's templates stay 404.
  • Rotation Problems Shows the Incident and Superseded Information — The incident_id, incident_status and superseded_at fields of GET /api/v1/dashboard/rotation/failures were never filled: their lookup used invalid SQL and its error was dropped. They now carry the linked rotation-failure incident and the completion of a later successful rotation, and a failing lookup answers 500 instead of silently omitting them.
  • Signing an SSH Key Works, and Ed25519 Create-and-Deploy and OpenSSH Ed25519 Import No Longer Fail — POST /api/v1/ssh/sign always answered 500 (the certificate was stored without its issuer), POST /api/v1/ssh/keys/create-and-deploy answered 500 for the offered Ed25519 type, and POST /api/v1/ssh/keys/import rejected an Ed25519 key in the OpenSSH format that ssh-keygen writes. All three now succeed; an unreadable public key answers 400, an unknown project 404, and each signing is audited (SIGN_SSH_KEY).
  • SSH Key Routes No Longer Return Database Text, and Answer 404/409 for Unknown or Duplicate Input — Generating a key pair without a project_id or for an unknown project, importing a key whose fingerprint is stored already, deploying an authorized key to an unknown agent and every other failing read or write of the SSH key pair and authorized key routes answered 500 (or 400) with the database error text. They now answer 400 (missing project_id, unsupported key type), 404 (unknown project, user or agent) and 409 (key already imported); other failures answer 500 Internal server error and are logged.
  • Exporting an Unknown SSH Private Key Answers 404 — POST /api/v1/ssh/keys/{id}/export answered 500 with the database error for an id that does not exist (and for a caller without ssh:admin). It now answers 404 and 403; the answer stays uncacheable, and the single audit entry ssh_key.export records the outcome and never the key.
  • Rotation Templates: Creating One From the Console No Longer Fails — Saving a new rotation template with the fields the form sends (name, description, numeric policy values, default flag) answered 500 because the notification and workflow lists were stored as null. They are now stored empty and the template is created (201).
  • Certificate Request: The State Field Now Takes Effect — The State or Province typed in the request form was sent as province, which the backend ignores, so the field never reached the certificate subject. It is now sent as state and appears in the requested subject.
  • OCSP answers for certificates that are not revoked — The responder looked for the issuing CA in a column that does not exist, so every serial without a revocation entry got HTTP 500 and only revoked certificates were answered. A certificate issued by the CA now gets status good; a serial the CA never issued still answers revoked (certificateHold).
  • ACME clients can POST to the ACME server again — The CSRF check rejected every ACME POST (new-account, new-order, finalize, ...) with 403 CSRF token mismatch, because ACME clients such as cert-manager send no browser token. /api/acme/ is now exempt, as /api/v1/scep/ is; each ACME request is still authenticated by its JWS signature.
  • Initializing or deleting an internal CA can no longer stall the server — The per-organization lock of the internal CA operations pinned a database connection per waiting request while the holder needed another one, so enough concurrent initialize or delete calls exhausted the connection pool and hung every request. The key pair is now generated before the lock and everything under the lock runs on one transaction; concurrent calls for one organization still create exactly one CA (the others answer 409).
  • A Hung HSM Device Is Probed Once, Not on Every Tick — The health monitor gave up on a probe that did not answer in 30 seconds and marked the device unhealthy, but the next tick started another probe of the same device while the first was still blocked, so a device that stayed hung added a goroutine (with a PKCS#11 module an operating-system thread) every minute until the server ran out of threads. A device whose previous probe has not ended is skipped and stays unhealthy; it is probed again once the call returns.
  • Internal CA: Unknown Organization Answers 404, Parameters Are Bounded, Concurrent Initialization Creates One CA — Reading the internal CA of an unknown organization answered 204 and initializing it answered 500 after generating a CA that nothing referred to; both answer 404 now and create nothing. validity_years over 100, an RSA key_size over 8192 (it kept the server busy generating the key) and a common_name over 255 characters answer 400. Initializing, repairing (initialize-ca) and deleting the internal CA of one organization run one after another, so two simultaneous initializations no longer create two root CAs (one answers 201, the other 409) and a delete is not repeated.
  • ACME EAB Credentials: Errors Answer 400 or 404 Instead of 500, and Create and Revoke Are Audited — Creating a credential without project_id (400), with an unknown project or template (404) or a label over 255 characters (400) answered 500 with the database error text; every failure now answers a fixed message, no database text. Creating and revoking a credential write the audit entries CREATE_EAB_CREDENTIAL and REVOKE_EAB_CREDENTIAL (the one-time HMAC key is in neither). Revoking a credential that is revoked already answers 409 instead of 200.
  • Certificate Follow-ups: Status Sync Uses the Account Settings, TLS Switches Need the Credentials — The scheduled status check of externally issued certificates now reads the account settings like the sync does (it used the provider defaults). Changing verify_ssl, insecure_skip_tls or tls_config of a CA account requires entering the credentials again, like a changed URL. Adopting a discovered certificate that matches several managed ones now also prefers a project-less one of the same environment. Listing HSM devices answers a plain 500 without error text and with a request id in the log.
  • One Unreachable HSM Device No Longer Stalls or Crashes the Health Monitor — The monitor probed every device in turn with no time limit and no panic protection: a device that never answered (a blocked PKCS#11 call) stopped the probing of all other devices, and a provider that panicked took the whole server down. Each probe now has a 30-second limit and its own recovery; a device that times out or panics is marked unhealthy and the monitor goes on.
  • CA Account Inputs Are Bounded and a Duplicate Name Answers 409 — The sync interval of a CA account is limited to a year (525600 minutes) and the renewal lead to 3650 days: larger values overflowed the next sync time, which made the scheduler sync on every tick. A display name longer than 255 characters answers 400, and connecting or renaming an account to a name already used by an account of the same organization and provider type answers 409; both answered 500.
  • Synchronising a CA Account Uses the Account's Settings — The sync of a CA account (the Sync button and the scheduler) ignored the account's settings, so an SSLmarket account configured with another base_url was always synchronised against the production API: the button answered 500 and the account was marked as in error. The sync now initialises the provider with the same settings as ordering and the product sync.
  • Changes to HSM Devices and Renewal Policies Are Audited for the Caller — Creating, updating and deleting an HSM device was recorded under the system user without the caller's address, and renewal policy changes were not recorded at all. Every change is now an audit entry for the user who made it, with the address (device configs never appear in it).
  • Renewal Policy Errors Carry No Database Text — Creating a renewal policy for an unknown organization answers 404 (it answered 500 with the foreign key constraint of the database); list, create, update and delete failures answer a generic 500.
  • The HSM Device Test Does Not Return the Provider's Error Text — The Test button returned the raw error of the provider (with the address it called and what answered) in error. It now returns a fixed text (the device could not be reached or its configuration is invalid; the device did not answer the key listing) and the details go to the server log.
  • HSM Device Refusals Are Clear Instead of Database Text — Creating or renaming an HSM device with a name already in use in the organization answers 409 (it answered 400 with the name of the database constraint), creating one for an unknown organization 404, and an unexpected failure 500 without text. Deleted devices keep their name.
  • A PKCS#11 HSM Device With an Unloadable Module No Longer Crashes the Test and the Health Probe — Testing a PKCS#11 device whose module library cannot be loaded dereferenced a nil pointer: the Test button answered 500, and the background health probe (which does not recover from panics) could have stopped the whole server. The device is now reported as unreachable.
  • Editing the Environments or Function Groups of an HSM Device No Longer Fails — PUT /api/v1/organizations/{id}/hsm-devices/{deviceId} with environments or applies_to (the Go SDK sends these fields) answered 400 with database text and stored nothing. The lists are now saved, an empty list means all, and applies_to is validated like on create (400 "invalid scope tag").
  • CA Account Input Errors Answer 400 or 404 Instead of 500 — Connecting or updating a CA account with credentials or settings the CA refuses, an unsupported provider type or an invalid ca_authority_id answered 500 "Failed to connect account" or "Failed to update account"; connecting with an invalid organization ID in the path answered 500 as well, and GET /api/v1/ca-providers/{type}/schema for an unknown provider type did too. They now answer 400 ("The connection to the CA failed: check the credentials and settings", "unsupported provider type: …", "Invalid ca_authority_id", "Invalid organization ID") and 404 ("CA provider not found"). What the CA answered is never part of the response (it can come from any host the settings point at) and is logged with the credential values removed.
  • Syncing the Products of a CA Account No Longer Fails — POST /api/v1/organizations/{id}/ca-accounts/{accountId}/products/sync (and the legacy /api/v1/ca-accounts/{id}/sync) answered 500 for every provider that reports key sizes or algorithms, and listing the stored products failed the same way: the allowed key sizes and algorithms were written to PostgreSQL array columns as JSON text. They are now stored and read as arrays, so the product catalog loads and the request modal lists the products.
  • Certificate Import No Longer Sends Internal Error Text, and a Wrong Private Key Answers 400 — The import routes (POST /api/v1/certificates/import, /import/bulk, /import/file, /import/preview-multi) answered 500 with the raw error text of the server (for example a database constraint message), and the items of a bulk or file import carried it in a 200 answer. They now answer 500 with a generic message (the cause goes to the server log with the request ID) and mark such an item with Failed to import certificate. A private key that does not match the certificate, a file that contains only a key and a certificate that does not belong to the targeted one are input errors and answer 400 with the reason instead of 500.
  • Submitting a Certificate Request With Input the Server Rejects Answers 400 or 404 — POST /api/v1/certificates/csr answered 500 ("Failed to submit CSR") for a CSR that cannot be parsed, has a bad signature or breaks the template's key size or SAN rules, for an unsupported csr_source_mode, an unknown template and an unknown CA account override. It now answers 400 with the reason for the first three and 404 for an unknown template or CA account; the same texts reach the project CSR route.
  • Deciding a Certificate Request That Is Unknown or Already Decided Answers 404 or 409 — Approving, rejecting or archiving a certificate signing request (POST /api/v1/certificates/csr/{id}/approve, /reject, /archive) answered 500 when the request did not exist in your organization, was already approved, rejected or archived (for example from a stale list), or was decided by someone else at the same moment. They now answer 404 for an unknown request and 409 for one that is no longer pending (or is archived already), nothing is changed, and two approvers deciding the same request at once issue the certificate only once.
  • Private Key Import for a Discovered Certificate No Longer Shows Database Errors — POST /api/v1/discovered-certificates/{id}/import-key answered 400 with the raw error of a failing database read or write (for example the text of an SQL error) in the same way as it answers an unreadable or non-matching key. A failure to read or save the certificate or to write the audit log now answers 500 with the generic Internal server error and the cause goes to the server log; errors about the key itself (not a PEM key, wrong password, key does not match the certificate) keep answering 400 with their reason.
  • Changes to Discovered Certificates Are Audited — Assigning, adopting, ignoring and archiving a discovered certificate, importing its private key and the bulk assign, adopt and ignore routes (/api/v1/discovered-certificates/...) wrote only to the certificate audit log of the service, so the audit log showed nothing about them. Each successful call now also writes one audit entry (discovered_certificate_assigned, _adopted, _ignored, _archived, _key_imported, discovered_certificates_bulk_assigned, _bulk_adopted, _bulk_ignored) with the user, the certificate and, for an assignment or adoption, the project. A refused call, an unknown certificate and a bulk adoption that adopted nothing write none, and the key import never records the key or its password.
  • Restore and Permanent Delete Refuse an Archived Discovered Certificate or Certificate Request That Is Unknown or Not Archived — Restoring or permanently deleting an archived discovered certificate or certificate request (/api/v1/archived/discovered-certificates/{id}/restore, /permanent and /api/v1/archived/certificate-requests/{id}/restore, /permanent) that does not exist or was restored meanwhile answered 500 with the error text. They now answer 404 (record not found) for an unknown row and 409 (discovered certificate is not archived, certificate request is not archived) for one that is not archived, and write no audit entry. A permanent delete now locks the row and checks it again inside the transaction, so a restore that commits first wins and the row is not deleted; other failures answer the generic Internal server error.
  • Unknown and Stale Discovered Certificates Answer 404 and 409 Instead of 500 — Assigning, ignoring, archiving or importing a key for a discovered certificate that no longer exists or was archived meanwhile (/api/v1/discovered-certificates/{id}/assign, /ignore, /archive, /import-key) answered 500 record not found, and so did ignoring a certificate that is already managed (invalid state transition). They now answer 404 Certificate not found, or 409 when the state does not allow the change or the certificate is archived already. The bulk assign and ignore routes check every certificate first, so a request that names an unknown or managed one changes none of them (before, the ones listed before it were changed).
  • Discovered Certificate Routes No Longer Send Internal Error Text — When the database failed, assigning, ignoring or archiving a discovered certificate (POST /api/v1/discovered-certificates/{id}/assign, /ignore, /archive), the bulk assign and ignore routes, the discovery statistics and the archived certificate list answered 500 with the raw error text of the service. They now answer 500 with the generic Internal server error, and the cause goes to the server log with the request ID.
  • Orchestrator Mode Answers 409 for Writes It Cannot Do — Importing project secrets (POST /api/v1/projects/{id}/secrets/import) in orchestrator mode, and storing a value through PUT /api/v2/secrets/{id} or POST /api/v2/secrets/migrate for a names_only secret, answered 500. They now answer 409 with the reason (for example "import is not supported in Orchestrator Mode"), and the migration refuses before it changes any secret, like rollback and copy already did.
  • Editing an Identity Provider Could Overwrite Its Stored Secret — PUT /api/v1/identity-providers/{id} stored the secret fields of the config as sent. The previous console sent back the **** mask it had been shown, which replaced the real Entra ID client_secret and broke the sign-in, and an edit that left the secret out or empty erased it. An update now keeps the stored client_secret, bind_password, api_token and token whenever the new value is empty, missing, __MASKED__ or starts with ****; any other value replaces it. A provider whose mask was already stored needs its secret entered again.
  • Archive Actions on an Unknown or Active Row Answered an Internal Error — Restoring or permanently deleting a user that no longer exists (for example one another administrator already removed) and permanently deleting a user that is not deleted answered 500 on POST /api/v1/archived/users/{id}/restore and DELETE /api/v1/archived/users/{id}/permanent; the same held for archiving, restoring and permanently deleting an unknown project (/api/v1/projects/{id}/archive, /restore, /permanent), where the restore of an unknown project even answered 200. They now answer 404 (record not found) for an unknown user or project and 409 for an active user, with the same error texts.
  • Creating a User With an Invalid Password or an Existing Email Answered an Internal Error — POST /api/v1/rbac/users answered 500 with a generic message when the password did not meet the password policy or a user with the same email already existed, so the Access Control page could only show a generic failure. It now answers 400 with the policy message (for example password must be at least 8 characters long) and 409 (user already exists).
  • Approval Policies Could Not Be Created Without Conditions — POST /api/v1/projects/{id}/approval-policies answered 500 (cannot unmarshal array into Go value of type map[string]interface {}) whenever the request had no conditions, which is what the Go SDK and the Terraform provider's approval_policy resource send, so every such create failed. A missing conditions now defaults to an empty object and the policy is created.
  • Approving or Rejecting a Settled Approval Request Answered an Internal Error — POST /api/v1/projects/approval-requests/{requestId}/approve and /reject answered 500 when the request was no longer pending, when the same user had already voted on a request that needs several approvers (the Approve button stays visible in that case), or when the request did not exist. They now answer 409 (request is not pending, already voted) and 404 (record not found) with the same error texts.
  • Project Tags and Notification Webhooks Could Not Be Saved — PUT /api/v1/projects/{id}/settings answered 500 with a database error text whenever tags or notification_webhooks contained at least one entry, because the lists were written in the wrong format for their JSON columns (an empty list stored an object instead of an array). Both lists are now stored as JSON arrays and returned unchanged by GET /api/v1/projects/{id}/settings. A project whose empty list had already been stored in the old format could no longer be opened, and its presence broke the project list for the whole instance; migration 000211 repairs such projects during the upgrade.
  • Environment State Updates Failed With an Internal Error — PATCH /api/v1/projects/{id}/environments/{env}/state answered 500 for every environment that has settings, because the change was recorded for a nil user that violates the last_modified_by foreign key. The change is now recorded for the acting user (also in the audit log of the environment settings update), so enabling, disabling or marking an environment non-existent works. Calls authenticated with a service-identity token still fail, because last_modified_by references users only.
  • Compose Kit Did Not Start: Database Without TLS, Docs Without HTTPS — With the default MAZEVAULT_ENV=production the backend refuses a database connection without TLS, but the kit's bundled PostgreSQL ran without TLS (sslmode=disable), so the backend never became healthy; the docs container only listened on port 80 while the kit published and health-checked HTTPS. The kit's PostgreSQL now uses TLS with the certificate from the certs volume and the backend and OCSP responder connect with sslmode=require; the docs container serves HTTPS on the kit's docs port (DOCS_PORT, default 8088) when a certificate is mounted and plain HTTP on port 80 otherwise (Helm unchanged). Existing Compose installations pick this up when the new kit's installer is run, which installs its docker-compose.yml and keeps the existing .env and data volumes (so does upgrade-mazevault.sh from the new kit).
  • CRL Download in DER Returned an Empty Body — GET /api/v1/ca/{id}/crl (default format=der) answered 200 with DER headers but no content; only format=pem returned the CRL. The endpoint now returns the DER-encoded CRL, decoded from the stored PEM.
  • Built-in Roles Lost Permissions Granted by Upgrades — On every start the backend reset the permissions of built-in roles to an older hardcoded list, so permissions added by upgrade migrations disappeared after the first restart: report:read/report:write, keytab:read (User, Secret Manager), dashboard:write, agent:admin, audit:read, config:read and project:write (Certificate and Secret Manager), and token:read. Customer changes to built-in roles were overwritten the same way. The backend now only creates missing roles and never changes existing ones; migration 000207_restore_system_role_permissions restores the intended permissions. It also grants token:read/token:write/token:delete to Project Admin and Secret Manager (migration 000193 had granted them to roles that no longer exist). The migration only adds permissions and never removes any, so existing role assignments and customer edits stay in place.
  • Agent Keystore Deployment Sent a Bundle the Agent Could Not Read — The orchestrated agent_keystore_sync step handed the agent a single exported_data blob, while the agent's CertificateSyncBundle expects certificate_pem, private_key_pem and chain_pem; every JKS / PKCS#12 / Windows-store rotation therefore failed on the agent with failed to decode certificate PEM, and the CA chain never reached the trust store. All deployment consumers (Kubernetes secret, Key Vault, agent keystore, agent file, Oracle wallet) now split the export with one shared helper — first certificate = leaf, remaining certificates = issuer chain, key separate — that also accepts the base64 form produced by a gateway round-trip, and the keystore step sends exactly the fields the agent deserializes (including post_install_command, which was nested in the wrong place before). This is a backend/gateway-only fix compatible with already-deployed agents.
  • agent_command Rotation Steps Timed Out Against Every Agent — The step forwarded an HTTP request through a proxy message type the agent never implemented, so service restarts and other command steps waited 30 s and failed. The step now uses the agent's agent_command request (allowlist-enforced) and honors timeout_seconds.
  • Agent File, WebLogic and Oracle Wallet Targets Could Not Execute — agent_file_sync, agent_file_backup and weblogic_restart were generated by the workflow builder but never registered (unknown step type), and the registered weblogic_deploy / oracle_wallet_deploy steps returned executed without contacting any host. These targets now run through real, registered steps (see New Features); the folded-in step types weblogic_restart and agent_file_backup are refused with a hint to rebuild the deployment target instead of reporting false success.
  • Web Server and Load Balancer Targets Were Skipped Silently — The workflow builder had no case for web_server and load_balancer targets, so a rotation with such a target reported success without deploying anything. They are now refused when saved and fail the workflow build with an explicit reason when already present (see Deployment Targets Are Validated When Saved).
  • Database Wallet Targets Could Be Created Without an Agent — The UI marked Database Wallet as not requiring an agent, while the deployment runs orapki on the database host through the agent; such targets could never execute. The UI and the API now require an agent for this target type, and target updates sent as JSON documents are serialized explicitly on every database driver (GORM does not run field serializers for map-based updates; the Postgres driver happened to encode the struct, SQLite rejected it).
  • Windows windows_root Target Imported a Private Key Into the Trusted Root Store — The Windows keystore installer built a PFX with the private key for every store and used the raw keystore type (windows_root) as the store path, which is not a valid Windows store name. windows_root and windows_ca targets now install public certificates only (leaf into the designated store, chain classified into Root / CA), windows_my receives the leaf + key PFX without the chain, and keystore types are mapped to the real My / Root / CA store names. The backend side matches this: the agent_keystore_sync step no longer requires a private key for windows_root / windows_ca (a CA certificate without a key can now be deployed to a trust store) and never includes private_key_pem in the bundle for those stores even when the export carries a key; JKS / PKCS#12 / windows_my still fail fast without a key. The UI value Windows Cert Store (keystore_type: windows) never matched anything the agent accepts; the editor now offers Windows — Personal (My), Trusted Root CAs and Intermediate CAs explicitly (no path, alias or store password for Windows stores), saved targets with the legacy windows value are canonicalized to windows_my at execution time, and agent_keystore targets are validated at save (JKS / PKCS#12 need keystore_path; unknown store types are refused).
  • Legacy Records Could Export the Wrong Certificate as the Leaf — Certificates imported before leaf_certificate_pem existed keep raw_certificate_pem exactly as uploaded, which may be a "root, leaf" bundle; the new chain resolver initially read that raw field before the leaf-first public_chain_pem, so such a record would have exported, displayed and deployed the CA certificate as the leaf (and reported an empty, complete chain). The leaf is now selected by the record's stored SHA-256 thumbprint wherever it sits in the stored material; without a thumbprint the pre-existing order (dedicated leaf field → first block of public_chain_pem → raw upload → DER) applies. Covered by a regression test with root-first raw bundles for both end-entity and CA records.
  • Discovery Merged Multiple End-Entity Certificates From One File — The agent reports one record per end-entity certificate found in a PEM file but sends the whole file as the body; the backend normalized the bundle with a "first non-CA block" heuristic, so a file holding two end-entity certificates (old + renewed, server + client) collapsed both reports into the first certificate and the second was never recorded. The reported fingerprint now selects the leaf inside the bundle, sibling end-entity certificates are excluded from that leaf's chain, and the heuristic is used only when no block matches.
  • Windows Personal Store Deployment Needed a Keystore Password That Does Not Exist — The Windows My store has no keystore password; the agent only needs one to wrap the temporary PFX it imports and deletes on the host, and PowerShell refuses an empty value. Targets of type windows_my without a configured password therefore failed at install time. The agent_keystore_sync step now generates a one-time random PFX password when none is configured (it is never logged or persisted, and an operator-provided password is still honored), so no meaningless secret has to be provisioned for Windows stores. On the agent, the PFX password is handed to PowerShell through the process environment instead of being interpolated into the script, so a password containing quotes or $ can neither break the import nor be echoed back in an error. When the deployment runs through a gateway, the one-time password is derived from the step's idempotency key, so a retried attempt presents the identical bundle and the agent replays the completed install instead of rejecting it as a different operation. Windows store names are resolved by one shared rule on the agent: My and any plain LocalMachine store name discovered or configured on the host (for example WebHosting, the IIS default on current Windows Server) are identity stores that receive the PFX; Root, CA, AuthRoot, TrustedPublisher and Disallowed are trust-anchor stores that receive public certificates only — a private key is never imported into them, including through the renewal and legacy metadata paths; a bundle that carries a private key for a trust-anchor store is refused with an explicit error instead of being installed half-way (the renewed key would otherwise be lost silently). Names that could alter a PowerShell script are refused. Works with already-deployed agents (the backend change alone fixes the install).
  • Inventory Issuer Candidates Were Not Identified by Thumbprint — The chain resolver's issuer lookup did not load thumbprint_sha256 for candidate CA records, so the thumbprint-based leaf selection introduced above did not apply to them and a legacy intermediate stored root-first was never offered as an issuer (the chain stayed incomplete — safe, but the completion did not happen). The column is now loaded and a regression test covers a root-first intermediate candidate.
  • Private Key Import Accepted Any Key for a Certificate Without a Body — POST /discovered-certificates/{id}/import-key skipped the key/certificate match when the managed certificate had no stored body (the shape every adoption produced before this release), so a mismatched key could be stored silently and only fail at deployment. The import now rejects such a request with an explicit error until the body is present.
  • Attaching a Key to a Body-less Managed Certificate Left It Without a Body — The import update path attached the key but did not store the leaf PEM / DER on records that had none; it now attaches the body, keeps a richer stored chain, and completes a missing chain from the inventory.
  • Certificate Import Took Identity From the First PEM Block — Bundles ordered root, intermediate, leaf (as emitted by some tooling) were recorded with the root's common name, serial and thumbprint while the stored body was the leaf. Identity and metadata now derive from the normalized leaf regardless of block order.
  • Multi-File Certificate Import Dropped Separate Key Files — Uploading a certificate file together with a separate .key file silently discarded the key (the UI claimed it had been paired during preview) and imported the certificate alone, so re-importing a managed certificate to add its key always failed with "already exists". The key is now paired on the backend and imported with its certificate.
  • Secret Rotation No Longer Fails on the Backup Foreign Key — Every secret rotation aborted in the background saga with insert or update on table "rotation_backups" violates foreign key constraint "rotation_backups_execution_id_fkey". The pre-rotation backup was created before the rotation execution row existed and never set execution_id, so the non-null foreign key rejected the insert and no rotation could complete. The backup is now created inside the execution (after the runtime probe, before the first mutating step) with a valid execution_id, and organization resolution is validated up front. The regression was invisible because the SQLite test harness does not enforce foreign keys and the constraint exists only in the SQL migration; a foreign-key-enforced regression test now covers the real insert path.
  • Rotation Template Migrations Handle the New Policy Fields Correctly — The rotation template persistence path and its migrations were corrected so the newly added policy fields (lead_time_days, grace_period_days, secret_custody_mode) round-trip consistently between the API, SDK, provider, and the stored catalog.
  • Rotation Template Update No Longer Panics on Partial Edits — The rotation template create/update request decoder dereferenced typed-nil pointers, so any request that omitted an optional field — the common case for a single-field edit such as changing only the interval — panicked and returned HTTP 500. The decoder is now presence-aware: omitted fields are skipped, an explicit 0 or false is preserved as an intentional value, and project_id can be set, changed, or cleared (with an explicit null) without affecting other fields.
  • Retired Agents Can Re-Enroll With an Authorized Replacement Token — Re-registration for an unregistered or revoked agent was rejected outright, so a replaced host could not come back even with the single-use token issued by reregister. Re-enrollment of a retired agent is now allowed only with a purpose-issued replacement token — distinguished by a new purpose discriminator on deployment tokens — while stale deployment tokens and recovery tokens still cannot resurrect it; on success the agent's retirement markers are cleared and the single-use token is consumed.
  • Remote Uninstall Fails Cleanly and Reports Runner Startup Failures — Remote uninstall now rejects a host with no supported package manager (apt-get/rpm) up front as unsupported, instead of reporting success after best-effort cleanup while the package remained installed. If the detached uninstall runner fails to spawn, the agent now reports that terminal failure back over the health channel — reconciling the already-accepted operation — instead of only logging it locally.
  • Keytab Rotation Fails Closed on Agent-Status Lookup Errors — A database error while checking a keytab deployment target's agent status is no longer treated as "inactive" (which silently skipped the deploy while still reporting the rotation completed). The rotation now fails closed on a lookup error and only skips a confirmed-inactive agent.
  • Rotation Backup Acknowledge Restricted to the Recovery-Required State — POST /rotation/backups/{id}/acknowledge now transitions only a backup that is actually in recovery_required state, returning 404 for an unknown id and 409 for any other lifecycle state, instead of silently mutating a backup in any state.
  • Terraform mazevault_deployment Import Restores the Organization — The deployment resource Read now repopulates organization_id from the server, so an imported deployment can be destroyed; previously the delete call sent an empty organization and was rejected with 400.
  • Helm Customer Chart Uses Redis Instead of Silently Falling Back to Memory — The customer Helm chart defaulted externalRedis.connectionQuery to abortConnect=false, a StackExchange.Redis (.NET) option that the Go backend's Redis client rejects (redis: unexpected option), so the backend fell back to the in-memory cache, gateway state could not use Redis, and the OCSP responder crash-looped on startup. The default is now empty, and the chart now refuses to render when connectionQuery contains abortConnect — customers who set it in their own values must remove it. The backend and OCSP responder also no longer log the Redis password when a Redis URL fails to parse or connect: URLs in connection errors are redacted and URL parse errors no longer echo the raw URL.
  • Certificate Note Rejected Below the Displayed 256-Character Limit — Saving a certificate Note failed for text well under the advertised 256 characters whenever it contained non-ASCII characters (for example Czech diacritics), and the detail view gave no feedback at all. The backend enforced the limit in UTF-8 bytes while the UI counter, the request validator, and the VARCHAR(256) column all count characters, so a 2-byte character such as ř consumed two units of the quota (≈240 characters was the effective ceiling for typical Czech text). The same byte-based check affected certificate tags (64) and the documentation URL (500). All three limits are now enforced in Unicode characters on every layer, the Note editor counts and truncates by code points (an emoji counts as one character), and the full 256 characters can be saved regardless of script. In addition, the Tags / Documentation / Note section no longer swallows API errors: validation and permission failures are shown inline next to the field, the editor keeps the draft open for correction, and in-flight saves cannot be submitted twice. Certificate import now applies the same tag normalization (trim, de-duplicate, 64-character / 50-tag limits) as the detail editor. The PUT /certificates/{id} request body (UpdateCertificateRequest) is now described in the OpenAPI specification. No schema change is required.
  • Managed Tokens Could Not Be Saved in Orchestrator Mode With Azure Key Vault — Creating or renewing a managed token (POST /tokens, POST /tokens/{id}/renew) in Orchestrator Mode failed with 500 failed to offload token value to external provider whenever the offload target was Azure Key Vault. The generic key-offload path named every value it had no certificate metadata for mazevault-keys/key-<unix-timestamp> — a path-style identifier designed for HashiCorp Vault that Key Vault rejects because secret names may only contain [0-9a-zA-Z-] (400 BadParameter: Method PUT does not allow operation 'key-…'). The same identifier was used for keytabs and for private keys imported through discovery (mazevault-keys/<cn>-<serial>, with dots from the CN), so those offloads failed on Azure as well. The identifier is now generated per provider: Key Vault receives a flat, readable, collision-free name (mazevault-token-<token-name>-<id>, mazevault-keytab-<principal>-<id>, mazevault-<cn>-<serial>) that is validated against the Key Vault contract before any request is sent, while HashiCorp Vault and AWS Secrets Manager keep the historical mazevault-keys/… layout unchanged. The token value itself is never altered — it is base64-encoded for transport exactly as before and round-trips byte-for-byte (covered by a test with shell metacharacters, whitespace and Unicode). Key Vault's hard per-secret limit (25 600 bytes of the encoded payload, ≈19 200 raw bytes) is now checked up front and reported as a 400 with the exact limit and the size of the submitted value, instead of a generic 500 from Azure; an explicit secret name that violates the Key Vault pattern is also refused as a 400 rather than being rewritten silently. Several latent defects in the same path were fixed together: the token service passed the tenant under the wrong metadata key (org_id instead of organization_id), so the offload integration was chosen system-wide — the first enabled secret-manager integration of any organization — and token reveal / delete resolved the integration without any tenant scope; integration lookup is now scoped project → organization and never widens to another tenant's vault (an explicitly selected integration_id that is missing or disabled is rejected instead of being replaced); token renewal deleted the previous external value before the new one was written, so a failed offload left the token without any value — the order is now write → persist → retire, a renewal that fails leaves the previous value intact, and a metadata insert that fails removes the value it had just offloaded; a stored Azure reference carrying a version segment would have created a new secret named after the version GUID on update — the bare secret name is now extracted correctly; the asynchronous secret-offload worker queried a non-existent is_active column on project_integrations (enabled is the real flag); key-custody events for tokens and keytabs now reference the owning record, key type and acting user instead of a random UUID; and a certificate serial shorter than eight characters no longer panics the name generator. No database migration, API or configuration change is required.
  • SCIM Provisioning Created at Most One User or Group — Creating a user or group through the SCIM routes (POST /api/v1/auth/scim/{providerID}/Users and /Groups) stored the identity without an id, so the first one got the all-zero id and every later one failed with 500 (external_identities_pkey). A SCIM user without an externalId (optional in SCIM, and not sent by Okta) was also taken for the previous one and answered 500 user already exists. Identities now get a generated id, and a user without an externalId is identified by its userName.
  • SCIM Provisioning Answers a Duplicate User With 409 and an Unknown User or Group With 404 — Creating a SCIM user that the provider already has (POST /api/v1/auth/scim/{providerID}/Users) answered 500 user already exists, and updating an unknown user (PUT .../Users/{id}) or patching an unknown group (PATCH .../Groups/{id}) answered 500 record not found. They now answer 409, 404 User not found and 404 Group not found, as the SCIM clients expect and as the read routes already did.
  • Terraform Export Answers 404 and 400 for Unknown and Malformed Ids — GET /api/v1/terraform/export/project/{id} and .../organization/{id} answered HTTP 500 for an id that names no project or organization, or is not a UUID. They now answer 404 (project not found, organization not found) and 400 (Invalid Project UUID, Invalid Organization UUID).
  • Integration Group Names Must Be Unique Within a Project (409) — POST /api/v1/projects/{id}/integration-groups and PUT /api/v1/integration-groups/{id} answered HTTP 500 with a database error when the project already has a group with that name. They now answer 409 an integration group with this name already exists in the project.
  • Integration Group List Answers 404 for an Unknown Project — GET /api/v1/projects/{id}/integration-groups answered 200 with an empty list for a project that does not exist. It now answers 404 project not found.
  • Integration Group Names Longer Than 255 Characters Answer 400 — POST /api/v1/projects/{id}/integration-groups and PUT /api/v1/integration-groups/{id} answered HTTP 500 with a database error for a name the column cannot hold. They now answer 400 name must be at most 255 characters.
  • Integration Group Create Answers 404 for an Unknown Project — POST /api/v1/projects/{id}/integration-groups answered HTTP 500 with the database's foreign-key error when the project does not exist. It now answers 404 project not found.
  • Integration Groups Can Be Saved Without Mappings — POST /api/v1/projects/{id}/integration-groups and PUT /api/v1/integration-groups/{id} answered HTTP 500 when the body had no mappings (the column does not accept null). A group without mappings is now stored with an empty list, and an update without mappings clears them.
  • Azure Discovery Failures No Longer Return Internal Error Text — A failing GET /api/v1/azure/keyvaults/discover or POST /api/v1/azure/keyvaults/check-access (for example for a user who is not logged in through Entra ID) sent the internal cause in details, and the Azure SQL server and permission summary routes sent the text of the failed Azure call with a 502. Failures now answer a fixed message and keep the cause in the server log. A project or visibility policy lookup that failed in the database on GET /api/v1/azure/app-registrations and /azure/subscriptions/{subscriptionId}/keyvaults answered 400 with the database text; it now answers 500 PROJECT_LOOKUP_FAILED or VISIBILITY_POLICY_FAILED without it.
  • Organization Integration Create and Update Answer 400 for a Refused URL — POST /api/v1/organizations/{id}/integrations and PUT /api/v1/organizations/integrations/{integrationId} answered 500 Internal server error when a URL was refused by the internal-address check (for example http://127.0.0.1/…). They now answer 400 with the reason (invalid integration config: field "url": …) and store nothing.
  • The Entra Scope Preview Works With a Saved Integration's Masked Secrets — Since the secrets of a project integration are answered as ********, the Entra scope preview in the integration wizard's edit mode sent the mask as the client secret and could not load the app list unless the secret was typed again. POST /api/v1/entra/projects/{id}/scope-preview takes an optional integration_id of the saved Entra ID integration (same project): secret fields sent as ******** or left out are taken from it, as long as its endpoint fields (tenant_id, client_id and the other endpoint fields) are unchanged; a changed endpoint needs the secrets entered again (400 invalid integration config), as does an integration of another project. The saved integration is judged with entra:read in its own environment and project (403, 404 when unknown). Without integration_id nothing changes.
  • Project Integration Create and Agent Bind Answer 4xx for Input the User Can Correct — POST /api/v1/projects/{id}/integrations answered 500 for a body of the wrong shape, a malformed project id and a name the environment already uses; it now answers 400 with the binding error, 400 Invalid Project UUID and 409 with the duplicate-name message; an Entra ID configuration its background authentication cannot use (for example background_auth_method client_secret without a client_secret) answers 400 with that message, as the update does. POST /api/v1/projects/{id}/integrations/agents answered 500 for an invalid environment, an unknown agent and an agent the project has already bound; it now answers 400, 404 Agent not found and 409.
  • Project Integration Updates and Session Routes Answer 400 for Input the Caller Can Correct — PUT /api/v1/projects/integrations/{integrationId} answered 500 when the new configuration of an Entra ID integration lacked what its background authentication needs (for example client_secret for client_secret); it now answers 400 with that message. The database session routes (…/sessions, …/sessions/terminate, …/sessions/drain) answer 400 integration is not a database integration for an integration of another type instead of 500.
  • An On-Behalf-Of Connection Test Without Entra ID Single Sign-On No Longer Fails With 500 — POST /api/v1/projects/{id}/integrations/test-connection with the on-behalf-of authentication method crashed the request (500) on a server without the Entra ID single sign-on service. It now answers 200 with success: false and OBO authentication is configured but Entra SSO service is not available, as the stored integration test does.
  • Deleting a Project Integration Archives Its Orphaned Secrets After the Delete — DELETE /api/v1/projects/integrations/{integrationId}?archive_orphans=true archived the secrets that lost their last integration inside the delete's database transaction: each archive waited for a second database connection while the transaction held one, and the archives stayed even when the delete then failed. The secrets are now archived once the delete has committed, and a failed delete archives nothing.
  • Organization Integration Update and Delete Answer 404 for an Unknown Integration — PUT /api/v1/organizations/integrations/{integrationId} answered 500 and DELETE answered 204 (and wrote a DELETE_ORG_INTEGRATION audit entry) for an id that names no integration. Both now answer 404 integration not found and audit nothing.
  • SCIM Group Patch Applies Capitalised Operations and Refuses What It Cannot Apply — PATCH /api/v1/auth/scim/{providerID}/Groups/{id} matched the operation name case-sensitively, so the Add and Remove that Entra ID sends changed nothing, and a member entry without value crashed the update, which was swallowed and answered 204 with nothing applied. Operation names are now case-insensitive (RFC 7644), and a patch with a member without the id of a user, an add without a value or an operation other than add and remove (for example replace) answers 400 in the SCIM error shape (scimType invalidValue or invalidSyntax) and changes nothing.
  • Password Request Approvals and Rejections Are Audited — Approving or rejecting an agent's keystore password request (POST /api/v1/admin/password-requests/{id}/approve and /reject) wrote no audit entry. They now record PASSWORD_REQUEST_APPROVED and PASSWORD_REQUEST_REJECTED (entity PASSWORD_REQUEST, the request id) with the acting user and IP; the entries never contain the password.
  • Password Requests Can Be Approved or Rejected Only Once — POST /api/v1/admin/password-requests/{id}/approve and /reject answered 200 for an unknown request and let an already approved or rejected request be resolved again (a rejected one could flip to approved, an approved one get a new password). They now answer 404 for an unknown request and 409 for one that is no longer pending, and the status check is part of the update, so two concurrent resolutions cannot both succeed. Nothing is audited for a refused call.
  • Unknown Secrets Answer 404 Instead of 500 — Updating a secret (PUT /api/v1/secrets/{id}), listing its versions, rolling it back (an unknown secret or version), and archiving, restoring or permanently deleting it answered 500 when the secret no longer existed (for example deleted in another browser tab); restoring an unknown secret even answered 200 and wrote a RESTORE_SECRET audit entry. They now answer 404, and a restore of an unknown secret writes no audit entry.
  • A Secret Key Can Be Created Again After the Secret Was Archived or Deleted — Creating a secret (POST /api/v1/secrets/) with the key of a secret that had been archived or deleted failed with 500 Failed to create secret: the unique index idx_secrets_unique_key_env_version also covered archived and deleted rows, although the create check only looks at live ones. Migration 000212 restricts the index to live rows (not archived, not deleted). Restoring an archived secret whose key was created again in the meantime now answers 409 instead of a database error.
  • Copying a Secret Is Audited — Copying a secret to another environment (POST /api/v1/secrets/{id}/copy) wrote no audit entry. It now records COPY_SECRET (entity SECRET, the new secret's id, with the source id and target environment in the details) with the acting user and IP; the entry never contains the value.
  • Restore and Permanent Delete Refuse a Secret That Is Not Archived — POST /api/v1/secrets/{id}/restore and DELETE /api/v1/secrets/{id}/permanent acted on any secret: a stale Archived tab (the secret had been restored or never archived) could permanently delete a live secret, and a restore of a live secret wrote a false RESTORE_SECRET audit entry. Both now answer 409 secret is not archived, change nothing and write no audit entry.
  • Restore and Permanent Delete Refuse a Project That Is Not Archived, and Restore a User That Is Not Deleted — POST /api/v1/projects/{id}/restore and DELETE /api/v1/projects/{id}/permanent acted on any project: a stale Archived tab (the project had been restored or never archived) could permanently delete a live project with all its secrets, and a restore of a live project wrote a false RESTORE_PROJECT audit entry. POST /api/v1/archived/users/{id}/restore did the same for a user that is not deleted (RESTORE_USER). All three now answer 409 (project is not archived, user is not deleted), change nothing and write no audit entry.
  • Permanent Delete Cannot Race a Restore — Permanently deleting a project, a user or a secret now locks the row and checks that it is still archived (or deleted) before it deletes, so a restore that commits at the same moment can no longer be followed by the irreversible delete of the now-live item (the delete answers 409); restoring a user is a single conditional update, so two restores no longer both succeed and audit. The retention cleanup of expired projects and secrets uses the same check.
  • Approvals and Role Assignment Answer 404 and 400 for a Typo Instead of 500 With Database Text — Creating an approval policy or an approval request for an unknown project (POST /api/v1/projects/{id}/approval-policies, …/approval-requests) answered 500 with the PostgreSQL foreign key error; updating an unknown policy (PUT /api/v1/projects/approval-policies/{policyId}) answered 500 record not found; assigning a role with an unknown user or role, or with an invalid environment in the scope (POST /api/v1/rbac/users/{id}/roles), answered 500 Internal server error. These are what the Go SDK, the CLI and the Terraform provider hit with a typo. They now answer 404 Project not found, 404 Policy not found, 404 user not found / 404 role not found and 400 invalid environment scope …, and no database text reaches the response.
  • Environment Settings of an Unknown Project Answer 404 — GET /api/v1/projects/{id}/environment-settings for a project that does not exist answered 500 LIST_FAILED; it now answers 404 Project not found.
  • Saving the Settings of an Unknown Project and Requesting Approval With an Unknown Policy Answer 404 — PUT /api/v1/projects/{id}/settings for a project that does not exist answered 500, and POST /api/v1/projects/{id}/approval-requests with a policy_id that does not exist answered 500 (a foreign key error). They now answer 404 Project not found and 404 Policy not found.
  • Server Errors of Projects, Approvals, Identity Providers, SAML, Archive and Password Requests No Longer Show Internal Text — A failing database call, or a SAML provider with an invalid SSO URL, answered 500 with the raw error text (a PostgreSQL message, record not found, a URL parse error) in the response body, also on the public SAML metadata and login routes. The text now only goes to the server log, and the body is Internal server error; the environment settings list keeps its LIST_FAILED code and message without details.
  • No False Audit Entries for Calls That Changed Nothing — A PATCH of Universal Auth with nothing to change no longer writes UPDATE_UNIVERSAL_AUTH, revoking a client secret that is already revoked (still 204) no longer writes a second REVOKE_CLIENT_SECRET and keeps the first revocation time, and deleting an identity provider id that does not exist (still 204) no longer writes DELETE_IDENTITY_PROVIDER.
  • Secret Import, Azure Key Vault Pull, Copy and Sync Conflict Resolution Ignore Archived Secrets — A key can exist as an archived and as a live secret at the same time (migration 000212). Project import, the Azure Key Vault pull, the "already exists" check of a copy and the resolution of a sync conflict found the secret by project, key and environment without excluding archived rows, so an import or pull could overwrite the archived secret and leave the live one unchanged, a copy could be refused because of an archived secret, and a sync conflict could be resolved on the wrong secret. They now only consider live secrets.
  • Rollback and Copy of a Secret in Orchestrator Mode Answer 409 With the Reason — Rolling a secret back to an earlier version (POST /api/v1/secrets/{id}/rollback, the history dialog) or copying it to another environment (/copy) in Orchestrator Mode answered 500 Failed to rollback secret / Failed to copy secret, hiding that the operation does not exist there (the value lives in the external provider). They now answer 409 with rollback is not supported in Orchestrator Mode / copying secrets is not supported in Orchestrator Mode.
  • Creating a Secret With an Invalid Environment or Unknown Project Answers 400/404 Instead of 500 — POST /api/v1/secrets/ answered 500 Failed to create secret for an environment identifier the platform does not accept (reserved such as default or all, shorter than 2 or longer than 32 characters, or not matching [A-Za-z][A-Za-z0-9_-]) and for a project_id that does not exist. This is what the Terraform provider (mazevault_secret.environment), the CLI and the Go SDK hit with a typo. It now answers 400 with the reason (invalid environment "x": environment slug is shorter than 2 characters) and 404 Project not found.
  • Unknown Secrets Answer 404 in the Secret Sync Routes — Forcing a sync (POST /api/v1/secrets/{id}/force-sync, /environments/{env}/force-sync), reading the sync status (GET /api/v1/secrets/{id}/sync-status), the offload sync (POST /api/v1/secrets/{id}/sync) and the offload status (GET /api/v1/secrets/{id}/offload-status) answered 500 when the secret no longer existed or was archived (for example deleted in another browser tab). They now answer 404.
  • Forcing a Sync for an Invalid Environment Answers 400 — POST /api/v1/secrets/{id}/environments/{env}/force-sync answered 500 when env was reserved or not a valid environment slug. It now answers 400 with the validation message.
  • Secret Sync Routes No Longer Send Internal Error Text — When a database error occurred, force sync (POST /api/v1/secrets/{id}/force-sync, /environments/{env}/force-sync), the sync status (GET /api/v1/secrets/{id}/sync-status), the sync log (GET /api/v1/secrets/{id}/sync-logs) and drift detection (POST /api/v1/secrets/drift/detect) put the raw error (table names, SQL state) in the response body. They now answer 500 with a generic message; the cause is only logged.
  • Secret Syncs and Drift Detection Are Audited — Forcing a sync for one environment (POST /api/v1/secrets/{id}/environments/{env}/force-sync), the offload sync of a secret (POST /api/v1/secrets/{id}/sync) and drift detection (POST /api/v1/secrets/drift/detect) wrote no audit entry. They now record FORCE_SYNC_SECRET_ENVIRONMENT, SYNC_SECRET and DETECT_SECRET_DRIFT with the acting user and IP (drift detection with its project scope and the result counts); the entries never contain a secret value. Refused calls write nothing.
  • The Agent's Token Request to /oauth/token Was Refused With 403 — POST /oauth/token (the client-credentials endpoint documented in the API guide and used by the agent) answered 403 CSRF token mismatch to every request without an Authorization header or the browser's XSRF cookie and header pair, which machine clients never send, so an agent configured with an OAuth client could not obtain a token. The CSRF check now exempts the endpoint like /api/v1/auth/token and /api/v1/auth/universal-auth/login: it authenticates with the client secret in the request body, not with a cookie.
  • Updating the Trusted IPs of Universal Auth Broke the Configuration — PATCH /api/v1/auth/universal-auth/identities/{id} with clientSecretTrustedIps or accessTokenTrustedIps failed with 500 for two or more addresses, and for a single address it stored the address as a JSON object instead of a list, after which the configuration could not be loaded any more: reading it, updating it, creating, listing or revoking client secrets and the universal-auth login all answered 500. The addresses are now stored as a list for any number of addresses (an empty list trusts no address, so every login with it is refused with 401 Invalid credentials; send 0.0.0.0/0 to allow any address), and a database migration (000213) converts configurations damaged by the old update back to lists. A PATCH for an identity without Universal Auth now answers 404 instead of 500.
  • SRP Login Answers 400 for an Invalid Client Public Key — POST /api/v1/auth/srp/init answered 500 failed to create SRP server when the client's public key A was 0 modulo the group prime N (for example 0, N or 2N) or made the scrambling parameter zero. The SRP library already refuses such a key (it would make the session key independent of the password); the route now reports it as 400 invalid public key A instead of an internal error.
  • Unknown Consistency Groups, Warnings and Projects Answer 404 — Reading (GET /api/v1/consistency-groups/{id}), updating (PUT /api/v1/consistency-groups/{id}) and checking (POST /api/v1/consistency-groups/{id}/check) a consistency group that no longer existed, and creating a group in a project that does not exist (POST /api/v1/projects/{id}/consistency-groups), answered 500. Resolving a warning that does not exist (POST /api/v1/consistency-warnings/{id}/resolve) answered 200 without changing anything. They now answer 404.
  • Resolving a Consistency Warning Works — POST /api/v1/consistency-warnings/{id}/resolve always answered 500: it recorded a resolving user that was never read from the request, which the database refused. The warning is now marked resolved by the signed-in user, with the notes.
  • A Duplicate Consistency Group Name Answers 409 — Creating (POST /api/v1/projects/{id}/consistency-groups) or renaming (PUT /api/v1/consistency-groups/{id}) a consistency group to a name the project already uses answered 500 with the database error. It now answers 409 with a message.
  • A Consistency Group Name Is Free Again After Its Group Is Deleted — Creating a consistency group (POST /api/v1/projects/{id}/consistency-groups) under the name of a deleted group failed, because the unique name index also covered deleted groups. This broke a Terraform replace of mazevault_consistency_group. Migration 000214 makes the index cover live groups only; two live groups of a project still cannot share a name.
  • Consistency Routes No Longer Send Internal Error Text — When a database error occurred, the consistency group and warning routes (/api/v1/projects/{id}/consistency/*, /api/v1/consistency-groups, /api/v1/consistency-warnings) put the raw error (table names, SQL state) in the response body. They now answer 500 with a generic message; the cause is only logged.
  • Consistency Group Creation and Deletion Are Audited — Creating a consistency group (POST /api/v1/projects/{id}/consistency-groups) and deleting one (DELETE /api/v1/consistency-groups/{id}) wrote no audit entry, unlike updating one. They now record consistency_group_created (with the project) and consistency_group_deleted with the acting user and IP. Refused calls and the delete of a group that no longer exists write nothing.
  • Archived Secret List No Longer Sends Internal Error Text — When a database error occurred, GET /api/v1/archived/secrets put the raw error (table names, SQL state) in the response body. It now answers 500 with a generic message; the cause is only logged.
  • Password History Hashes Never Leave the Backend — The secret responses (list, update, copy, archive list) no longer carry the password_history_hashes field. It held hashes of earlier secret values and was empty in practice; nothing reads it, so no client needs a change.
  • The v2 Secret Routes Answer Unknown and Duplicate Input With 4xx — POST /api/v2/secrets answered 500 for a name (blind index) that already exists in the environment, an invalid environment and an unknown project; it now answers 409, 400 (with the message) and 404. PUT /api/v2/secrets/{id} answers 404 for an unknown or archived secret, and GET /api/v2/projects/{id}/secrets and /secrets/matrix answer 404 for an unknown project (all were 500). A secret that POST /api/v2/secrets/migrate cannot migrate is reported with a fixed text (secret not found, migration failed, ...) instead of the raw database error, and an archived secret is no longer migrated.
  • The Migration of Secrets to Zero-Knowledge Is Audited — POST /api/v2/secrets/migrate rewrote the encrypted name and value of secrets without an audit entry. Each migrated secret now records MIGRATE_SECRET_V2 with the user and IP address and the first 8 characters of its blind index; no ciphertext or nonce is audited.
  • The Access Log No Longer Records a Blind Index — GET /api/v2/secrets?project_id=…&blind_index=… wrote the whole blind index (the HMAC of a secret name) into the access log's query field on every lookup by name. The value is now logged as [REDACTED]; the lookup itself is unchanged.
  • Secret Comparison Checks the Read Permission of Both Environments — POST /api/v2/secrets/compare (by blind index and by key) only needed secret:read in the request's environment, so a user could learn which secrets exist, and in which versions, in environments they may not read. It now needs secret:read on source_env and on target_env and answers 403 insufficient permissions for environment otherwise, like the other v2 read routes.
  • A Project Can Hold Several Zero-Knowledge Secrets per Environment — The unique index of secret keys also covered the empty key of a zero-knowledge (v2) secret, so a project and environment could hold one v2 secret only: creating a second one with POST /api/v2/secrets answered 500. The index now covers only secrets that have a key (migration 000215, idempotent); a repeated blind index is still refused with 409.
  • Archiving, Restoring, Deleting and Unrevoking a Certificate in the Wrong State Answer 409 or 404 — POST /api/v1/certificates/{id}/archive for a certificate that is archived already answered 200 and wrote another audit entry; POST …/restore for a certificate that is not archived (a stale Archive list) answered 200 and wrote an audit entry; DELETE …/permanent of a certificate that is not archived deleted it for good; and an unknown certificate answered 500 with the database text (or 200 for a restore). They now answer 409 (certificate is already archived, certificate is not archived) or 404 and change nothing, and a refused call writes no audit entry; the permanent delete locks the row and re-checks it inside the transaction. POST …/unrevoke answers 404 for an unknown certificate and 409 for one that is not revoked or was not put on hold (reason code 6) instead of 400, and revoking and unrevoking lock the certificate, so two simultaneous calls cannot both succeed.
  • Certificate Routes No Longer Send Internal Error Text — When a database error occurred, archiving, restoring, permanently deleting, revoking or unrevoking a certificate (/api/v1/certificates/{id}/…) and listing its rotation targets put the raw error (table names, SQL state) in the response body, and reading a certificate or its status answered 404 for any failure. They now answer 500 with a generic message (only a missing certificate is 404); the cause is only logged.
  • Revoking a Certificate That No CA Issued Answers 409 Instead of 500 — POST /api/v1/certificates/{id}/revoke (and POST /api/v1/projects/{id}/certificates/{certId}/revoke, which now revokes through the same code) answered 500 with certificate has no associated CA for a certificate without a CA of MazeVault, such as an imported one, which the Revoke button offers. It now answers 409 with the same message.
  • Updating a Certificate in Breach of a Template Rule Answers 409 or 400 Instead of 500 — PUT /api/v1/certificates/{id} answered 500 Failed to update certificate when the auto-renew settings were edited on a certificate whose renewal settings its template governs, and when the template belonged to another organization than the certificate's project. They now answer 409 (cannot manually edit renewal settings when certificate is governed by a template) and 400 (template belongs to a different organization than the target project, or a renewal_key_policy the service refuses) with the reason.
  • Removing a Certificate Hold Is Audited — POST /api/v1/certificates/{id}/unrevoke changed the certificate back to active without an audit entry, unlike revoking. It now records certificate.unrevoked (entity certificate) with the acting user and IP address. Refused calls write nothing.
  • The Project Revoke Route Revokes the Certificate It Names — POST /api/v1/projects/{id}/certificates/{certId}/revoke read the project id as the certificate id, so every revoke from the Certificates tab of a project answered 404 and nothing was revoked. It now revokes the certificate certId in the project of the path; a certificate of another project answers 404 (Certificate not found in this project), one that is revoked already or that no CA of MazeVault issued answers 409.
  • Project PKI Routes No Longer Send Internal Error Text — When a database call or a secret manager failed, listing the CAs, CSRs or certificates of a project, creating a project CA (/api/v1/projects/{id}/ca, /csr, /certificates) and listing the secrets of an integration (/api/v1/projects/{id}/integrations/{integrationId}/secrets) put the raw error (table names, SQL state, provider text) in the response body. They now answer 500 with a generic message in the same shape; the cause is only logged.
  • Approving or Rejecting a Project CSR That Is Gone or Decided Answers 404 or 409; Assigning a Missing Template Answers 404 — POST /api/v1/projects/{id}/csr/{csrId}/approve and …/reject answered 500 for a CSR that was unknown, belonged to another project, or had been decided or archived by someone else meanwhile (a stale list in the CSR Approvals tab). An unknown or foreign CSR now answers 404, one that is no longer pending 409 with its state; the request is claimed with a conditional update (approve) or locked and re-read (reject), so two approvers cannot both act on it. POST /api/v1/projects/{id}/templates/{template_id} answered 500 for a template or project that no longer exists and for a template of another organization; it now answers 404 and 400 (template and project belong to different organizations).
  • Project CSR Routes Work With an API Token — An API token request carries no organization into the project CSR routes, so POST /api/v1/projects/{id}/csr/{csrId}/approve and …/reject looked for requests of no organization and could not find any, and a CSR submitted with a token (POST /api/v1/projects/{id}/csr) was stored without an organization, so nobody could approve it. The routes now use the organization the permission check gives the request (the default organization for a token, as for a session).
  • Project CSR and CA Initialization Changes Are Audited — Submitting (POST /api/v1/projects/{id}/csr), approving and rejecting a project CSR and initializing a project CA (POST /api/v1/projects/{id}/ca/initialize) wrote no audit entry (initializing a CA only wrote two debug entries about the project type, one of them before the request was validated; both are removed). They now record SUBMIT_PROJECT_CSR, APPROVE_PROJECT_CSR, REJECT_PROJECT_CSR and INITIALIZE_PROJECT_CA with the acting user, the request or CA and the IP address; a refused call records nothing.
  • Initializing the CA of an Unknown Project Answers 404 — POST /api/v1/projects/{id}/ca/initialize answered 500 (Failed to get project) when the project does not exist or is archived. It now answers 404 (Project not found).
  • Renewal Orders Waiting for Domain Validation Can Be Stored — The status check of the renewal queue allowed nine statuses, but domain validation (pending_dcv), the ADCS approval wait (awaiting_adcs_approval), the CA follow-up wait (awaiting_ca_followup) and manual_review are set as well, so writing one of them failed and an order never reached the DCV panel of the project Certificates tab. Migration 000217 (idempotent) allows all thirteen statuses the code uses.
  • Renewal Queue Actions in the Wrong State Answer 409 or 404 — Cancelling a completed or cancelled renewal (POST /api/v1/renewal-queue/{id}/cancel) answered 200 and reopened nothing but rewrote the status, cancelling an unknown item answered 200, approving an item that is not pending or an unknown one answered 500, resuming an unknown item answered 409 without a reason, the DCV panel answered 200 with an empty list for an unknown item and 400 with internal text (item and certificate ids, record not found) for a wrong status, a missing order or a certificate without a CA account, renewing or reading the history of an unknown certificate answered 500 or an empty list. They now answer 404 for an unknown item or certificate and 409 with a short message for a wrong state, and change nothing; a failed call of the CA itself is still 400 with the CA's text, any other failure is a generic 500.
  • Simultaneous Renewal Calls Cannot Run Twice — Resuming a failed renewal read the item, then deployed and only afterwards marked it deploying, so two simultaneous POST /api/v1/renewal-queue/{id}/resume calls could both deploy to the targets, and two simultaneous POST /api/v1/certificates/{id}/renew calls could both find no open renewal and queue two (two paid CA orders). The resume now claims the item with a conditional update before anything is deployed (the other call answers 409), and a renewal is queued under a lock of the certificate row.
  • Renewal DCV and Backfill Routes Send No Internal Error Text — When a database call failed, GET /api/v1/renewal-queue/{id}/dcv and POST …/dcv/retry answered 500 with the raw error and POST /api/v1/admin/certificates/backfill and …/backfill-aia put it in the message of the answer. They now answer a generic 500; the cause is only logged.
  • Certificate Backfills Are Audited — POST /api/v1/admin/certificates/backfill and …/backfill-aia rewrite metadata of certificates of every project but wrote no audit entry. They now write BACKFILL_CERTIFICATES and BACKFILL_CERTIFICATE_AIA entries with the user, the address and the number of updated certificates; a refused call writes none.
  • A Malformed Organization ID Is a 400 on the Renewal Queue — GET /api/v1/renewal-queue/?organization_id=… answered 500 for a value that is not a UUID. It now answers 400 (Invalid organization ID).
  • CRL Numbers Are Never Issued Twice — Regenerating a CRL read the CA's last CRL number and wrote the next one later, so two regenerations at the same time (a request and the scheduler, or two requests) could issue CRLs with the same number, which clients cannot order. The number is now claimed with one conditional update before the CRL is signed; a number whose CRL then fails to sign stays unused, which RFC 5280 allows.
  • CRL Regeneration Is Audited and Answers 404 for an Unknown CA — POST /api/v1/ca/{id}/crl/regenerate answered 500 with the service's internal text (failed to get CA: NOT_FOUND: …) for an unknown CA and for a CRL that could not be signed, and wrote no audit entry. An unknown CA now answers 404 CA not found (403 for a caller whose role is limited to an organization, see "Certificate Authority Routes Stay in the Caller's Organization"), a signing failure a generic 500 Internal server error (the cause goes to the log only), and every regeneration is recorded as REGENERATE_CRL with the user, the CA and the kind and number of the CRL; a refused call records nothing.
  • CA HSM and External Configuration Answer 404 for an Unknown CA and Take Effect at Once — POST /api/v1/ca/{id}/hsm, …/external/configure, …/external/test and …/external/templates/sync answered 500 for a CA that does not exist (the external configuration even tried to store a row for it). They now answer 404 CA not found (403 for a caller whose role is limited to an organization, see "Certificate Authority Routes Stay in the Caller's Organization"). Configuring the HSM changes only the HSM columns of the CA in one update instead of writing back the whole row it had read, so it can no longer undo a concurrent change (for example the OCSP URL), and it drops the CA's cached provider, so signing uses the new configuration at once instead of after the cache expires. The external configuration and the update of the CA are one transaction; no half-written configuration remains when the update fails. Configuring an external CA never succeeded before (it answered 500 Failed to configure external CA because it wrote CA columns that do not exist); it now records the provider in the CA's type, provider_type and provider_status and answers 200.
  • CA Configuration Changes Are Audited — Configuring the HSM or an external CA of a CA and synchronizing its templates (POST /api/v1/ca/{id}/hsm, …/external/configure, …/external/templates/sync) wrote no audit entry. They are now recorded as CONFIGURE_CA_HSM, CONFIGURE_CA_EXTERNAL and SYNC_CA_TEMPLATES with the user, the CA and the provider type (or the number of templates), never the PIN, credentials or other configuration values; a refused call records nothing.
  • The External CA Connection Test Needs ca:admin and Is Audited — POST /api/v1/ca/{id}/external/test contacts the external CA and writes its status, but accepted ca:read. It now requires ca:admin like the external CA configuration (a read-only caller gets 403) and records TEST_CA_EXTERNAL in the audit trail with the user and the CA.
  • The CA HSM Form Configures a Usable HSM — POST /api/v1/ca/{id}/hsm took only the flat HSM configuration, so the console's {provider, config} body answered 200 but stored an enabled HSM with no provider, which broke the CA (CRL regeneration and signing failed) and could not be undone. The route now accepts the console body (the provider becomes the type, config the fields; the flat form with type still works) and answers 400 for an empty or unsupported provider type (pkcs11, aws_kms, azure_kv, gcp_kms, trezor, ledger) before it changes anything.
  • Root CA Initialization Rejects Key Sizes and Validities That Cannot Work — POST /api/v1/ca/initialize and POST /api/v1/projects/{id}/ca/initialize answered 500 for an RSA key smaller than 2048 bits, accepted a negative or absurd validity and ran a key generation of any size for minutes (a size of a million bits). They now answer 400 with the reason for an RSA key size outside 2048–8192 and for valid_years outside 1–100, before anything is generated or stored.
  • Root CA Initialization Is Audited — Creating a root CA with POST /api/v1/ca/initialize wrote no audit entry. It is now recorded as INITIALIZE_CA with the user, the new CA and its name (never key material); a refused call records nothing.
  • Certificate Template Routes Answer 404 for an Unknown Template and 400 for a Bad Organization — GET, PUT and DELETE /api/v1/certificate-templates/{id} answered 500 for a template that does not exist (or was deleted), and a DELETE of nothing answered 204 and wrote a DELETE_TEMPLATE audit entry. They now answer 404 Template not found, and only a delete that deleted is audited. A malformed organization_id on the list and create routes answers 400 Invalid organization ID instead of 500. Updating a template locks its row for the read-modify-write, so two simultaneous updates no longer overwrite each other's fields.
  • A Certificate Template Takes Only a CA Account and Product of Its Organization — POST /api/v1/certificate-templates/ and PUT …/{id} stored the ca_account_id and ca_product_id of the body without checking them, so a template could be set to issue from a CA account of another organization (or one that does not exist), which then ordered certificates with that organization's account. The account must belong to the template's organization and the product to that account (or, without an account, to an account of the organization); otherwise the route answers 400 and stores nothing. The same check applies when a project template creates or updates its certificate template (see "Project Templates Stay in Their Organization").
  • Concurrent Renewals No Longer Exhaust the Database Connections — Starting a renewal (the Renew button, POST /api/v1/certificates/{id}/renew, and the renewals of the rotation service) locks the certificate and then read the key reuse policy through the shared connection pool, so each renewal held one connection and waited for a second one. With enough renewals at once (about as many as the pool has connections, 25 by default) every connection was held by a renewal waiting for another, and the whole backend stopped answering. The policy is now read on the connection of the renewal's own transaction.
  • Rejecting a Project CSR Does Not Hold the Database While Its Key Is Deleted — POST /api/v1/projects/{id}/csr/{csrId}/reject locked the request in a transaction and deleted its externally stored key inside it, through services that use the shared connection pool, so a reject held one connection while it waited for a second. Enough of them at once exhausted the pool. The request is now claimed (status rejected) in a short transaction and its key is deleted afterwards; a request approved in the meantime can no longer lose its key, and when the key cannot be deleted the rejection is undone (the CSR stays pending and the call answers 500).
  • Renewal Approvals and Triggers Are Audited Whatever the Run Does — POST /api/v1/renewal-queue/{id}/approve and POST /api/v1/certificates/{id}/renew wrote their audit entry (APPROVE_RENEWAL, TRIGGER_RENEWAL) only when the whole renewal succeeded, although the approval is committed, and the queue item exists, before the renewal runs. An approval or a trigger whose run failed, waits on the CA (the normal outcome with DigiCert and SSLMarket) or lost the item to another worker left no trace. Both are now audited as soon as they are committed, with how the run ended (finished, waits on the CA or an operator, failed, taken over by another worker); a refused approval and a trigger that only found a renewal already in progress change nothing and are no longer audited (the second was audited before).
  • The Certificate Rotation Routes No Longer Answer Database Errors, and a Failing Lookup Is No Longer a 404 — The rotation config, history, preflights and deployment targets of a certificate answered a failing database or rotation service with its error text in the 500 body, and the eligibility, execution and target reads answered any database error as 404 "not found". They now answer the fixed "Internal server error" body and keep 404 for records that do not exist.
  • Certificate Deployment Targets and Rotation Config Changes Are Audited — Creating, updating and deleting a deployment target (POST, PUT, DELETE /api/v1/certificates/{id}/targets…) and updating the rotation config (PUT /api/v1/certificates/{id}/rotation/config) wrote no audit entry. Each now writes one (certificate_rotation_target_created, …_updated, …_deleted, certificate_rotation_config_updated) with the names of the changed fields, never their values, and outcome=success, or outcome=failed when the change failed on the server.
  • A Malformed Certificate ID on the Certificate Target Routes Is a 400 — GET, PUT, DELETE /api/v1/certificates/{id}/targets/{targetId}, GET …/status and POST …/sync ignored the certificate id in the path, so a malformed one answered as if it were valid (200 or 204). They now answer 400 "Invalid certificate ID", as the other certificate routes do.
  • SSH Discovery Assignment Acts Only on Keys of the Caller's Organization — POST /api/v1/ssh/discovery/{id}/assign, /ignore, /bulk/assign and /bulk/ignore acted on any discovered key. They now act on a key only when the agent that found it belongs to the caller's organization (a key of an agent with no organization needs a permission that is not limited to an organization); any other key, like an unknown one, answers 404 Key not found, and a bulk request containing one changes none of its keys. A project of another organization or an unknown project or user in the assignment answers 404. The inbox list, its statistics and the match suggestions stay global.
  • A User's SSH Keys Are Read Only Within the Caller's Organization — GET /api/v1/rbac/users/{id}/ssh-keys listed every discovered key assigned to the user, including keys found by agents of other organizations. It now lists only the keys found by agents of the caller's organization (keys of agents with no organization only for a permission that is not limited to an organization).
  • SSH Rotation Configs Are Judged Through Their Key Pair — GET/POST /api/v1/ssh/rotation, GET /api/v1/ssh/rotation/key/{key_pair_id}, PUT and DELETE /api/v1/ssh/rotation/{id} showed and changed the rotation config of any key pair. A config is now reached only through its key pair: the key pair's project must belong to the caller's organization (or to one where the caller holds the permission, as a role not limited to an organization does) and only the key pair's owner or an SSH administrator sees it; the list holds only such configs and any other config answers like a missing one (404). Updating or deleting a config that does not exist answers 404 (deleting it twice used to answer 204), and creating one for an unknown key pair answers 404 key pair not found (it was 500).
  • SSH Discovery and SSH Rotation Errors No Longer Carry Database Text — A failure behind /api/v1/ssh/discovery, /api/v1/ssh/rotation and /api/v1/rbac/users/{id}/ssh-keys answered 500 with the driver's error text; it now answers {"error":"Internal server error"} and the cause is only logged. Creating a rotation config for a key pair that has one answers 409 (it was 500). A rotation config of a key pair that cannot be read (GET /api/v1/ssh/rotation/key/{key_pair_id}) answers 500, not 404 rotation config not found, so a database failure is not shown as "no config yet".
  • Agent SSH Route Errors No Longer Carry Database Text — A failure behind the agent's authorized-key poll and confirmation (/api/v1/agents/me/authorized-keys/..., /agent/{agent_id}/authorized-keys/...), its SSH discovery report (POST /api/v1/agents/me/ssh-discovery) and POST /agent/discover answered 500 with the driver's error text; it now answers {"error":"Internal server error"} and the cause is only logged.
  • Agent Key Deployments, SSH Discovery Reports and Agent Discovery Are Audited With Their Outcome — An agent's confirmation of an authorized key deployment (SSH_AUTHORIZED_KEY_DEPLOYMENT_CONFIRMED, outcome deployed or failed), its SSH discovery report (SSH_DISCOVERY_REPORT_SUBMITTED, the number of keys) and POST /agent/discover (AGENT_DISCOVERED, outcome created, updated, or refused for an unregistered or revoked agent) now write an audit entry.
  • SSH Discovery Assignment and SSH Rotation Config Changes Are Audited With Their Outcome — Assigning or ignoring discovered SSH keys (single and bulk) and creating, updating or deleting an SSH rotation config now write an audit entry with the outcome (assigned, ignored, created, updated, deleted) and one for a refused or failed request too (refused, failed). Updating a rotation config writes only the interval and the status and accepts only the status active or paused (400 otherwise); assigning a key no longer rewrites the fields an agent report refreshes.
  • Replacing Keytab Data and Saving the Keytab Cipher Policy Work Again — PUT /api/v1/keytabs/{id} with new keytab_data and PUT /api/v1/keytabs/cipher-policy answered 500 for every valid request (the encryption types and the cipher lists were written to the database in a form it rejects), so a keytab could not be replaced and the cipher policy of an organization could not be changed. Both now succeed: the replacement stores the keytab, its encryption types and its compliance, the policy is saved and decides the compliance a refresh stores.
  • Keytab Errors No Longer Carry Database Text, and Correctable Input Answers 4xx — A failure behind /api/v1/keytabs (listing, reading, updating, deleting, history, compliance, cipher policy, discovered keytabs, dashboard) answered 500 with the database error appended to the message, and a database error while reading a keytab answered 404. They now answer 500 Internal server error (the cause is logged only) and a database error is no longer read as an unknown keytab. Input the caller can correct is a 400/404 with its message instead of a 500: a replacement keytab_data that is no keytab file (400), an unknown enforcement_mode of the cipher policy (400, and the refused policy is no longer stored), and the download of a keytab that stores no keytab file, such as one made from a discovery (404).
  • Keytab Changes Are Audited With Their Outcome, and a Keytab Update Takes Only Known Statuses — Updating and deleting a keytab, saving the cipher policy and refreshing the cipher compliance (PUT and DELETE /api/v1/keytabs/{id}, PUT /api/v1/keytabs/cipher-policy, POST /api/v1/keytabs/refresh-compliance) left no audit entry. They now write keytab.update, keytab.delete, keytab.cipher_policy_update and keytab.compliance_refresh entries that end with their outcome, and a refused or failed update, delete, download, import or discovered import is audited too (outcome: refused or outcome: failed); no entry holds a keytab value. A keytab update with a status that is none of active, inactive, expired, revoked and discovered answers 400 (it stored any text before). Replacing the keytab file no longer reads the cipher policy while its transaction holds a database connection, so concurrent replacements cannot exhaust the connection pool, and two concurrent updates of one keytab archive its versions one after the other.
  • Managed Token Routes Work for API Tokens Without ?organization_id= — The managed token routes (/api/v1/tokens) read the organization only from a session or a JWT, so an API token (Terraform provider, CLI, automation) answered 400 organization context required for every request unless it sent ?organization_id=. They now act in the organization of the API token like a session does; the query parameter keeps precedence, and the permission check still judges it.
  • Managed Tokens Are Judged in Their Organization and Project — Creating a managed token with a project_id that does not exist answered 500, and one naming a project of another organization was accepted. A new token now needs a project of the caller's organization (404 otherwise) and the permission on it, and every by-id token route (read, reveal, update, renew, delete and the targets) judges the permission in the token's project, or in its organization when it has none, so a role limited to one organization is refused (403) on a token bound to a project of another organization. The lists keep holding the caller's organization only.
  • Revealing a Managed Token Is Never Cached — GET /api/v1/tokens/{id}/value (the deliberate reveal of a stored token, which needs token:admin) now answers Cache-Control: no-store, so a browser or proxy cannot keep the value. The reveal stays audited (the entry never holds the value), and no other token route and no service identity route other than the create (which returns its new client secret once) carries a stored value or secret.
  • Service Identity Errors No Longer Carry Database Text, and Token Renames Are Validated — A failure behind /api/v1/service-identities answered 500 with the database error as its message; it now answers 500 Internal server error (the cause is logged only), and a database error while reading an identity is no longer an unknown identity. Updating an unknown identity answered 500 record not found and deleting one answered 204; both are now 404. Renaming a managed token to an empty name is 400 and to the name of another token of the same organization and project 409, like the create (it was stored).
  • A Managed Token Target Must Name an Integration of the Token's Organization — Adding a deployment target to a managed token accepted any integration id. The integration of a new target now has to exist and belong to the token's organization (404 otherwise), and the caller needs token:write in the integration's own project (403 otherwise). The agent of a target is judged by the next entry.
  • Managed Token and Service Identity Changes Are Audited With Their Outcome, and Updates Take Only Known Values — A refused or failed update, renewal, delete, reveal, target change or create of a managed token and of a service identity left no audit entry; it now writes one that ends with its outcome (outcome: refused or outcome: failed), and the service identity entries end with created, updated or deleted; no entry holds a token value or secret. Updating a managed token with a status that is none of active, expired and revoked answers 400 (it stored any text before), and updating a service identity writes only its display name, description and owner e-mail, so a concurrent secret rotation is no longer overwritten. Deleting a token removes its rows in one transaction before its offloaded value is deleted.
  • Keytab Routes Work for API Tokens Without ?organization_id= — The keytab routes (/api/v1/keytabs) read the organization only from a session or a JWT, so an API token answered 400 organization context required for every request unless it sent ?organization_id=. They now act in the organization of the API token like a session does; the query parameter keeps precedence, and the permission check still judges it.
  • A Keytab Import Must Name a Project of the Caller's Organization — Importing a keytab, or a discovered keytab, with a project_id of another organization or an unknown one stored it unchecked (and the audit entry named the foreign project). The project now has to exist in the caller's organization (404 otherwise) and the caller needs keytab:write on it (403 otherwise); nothing is imported for a refused project.
  • A Managed Token Is Delivered Only to an Agent of Its Own Scope — A deployment target of a managed token could name any agent, so the token value could be sent to an agent of another organization or of a project the token does not belong to. A target that names an agent now needs an agent with an active integration of the token's project when the token has a project, otherwise an agent of the token's organization (404 otherwise); an agent without an organization never receives a token. Delivery checks the same, so a target created earlier with a foreign or organization-less agent fails without sending the value, and no error or output carries the token value.
  • Agent Discovery Results Page Shows the Discovered Data — The Discovery Results page of an agent (/agents/{id}/discovery) now shows the subject, issuer, expiry, location, source and the Managed status of each certificate from the stored discovery data. The previous page read fields the API does not return, so its cells were blank and every certificate showed "Unmanaged".
  • Multi-DC Sync Status shows the Source DC — The Source DC column of the Multi-DC Sync Status page (/multi-dc) read a field the backend never sends (source_dc instead of datacenter_id), so it stayed empty. It now shows the data center that produced each event.
  • Entra credential rotation history shows that credential only — The Rotation History button of a credential in an Entra application listed the executions of every credential of the application. It now lists the executions of that credential; the application's own Rotation History button still shows all of them.
  • Entra rotation settings cannot be saved over an unread configuration — The Configure Rotation dialog of an Entra credential showed default values when the stored configuration could not be read, and saving them replaced the stored post-rotation actions with an empty list. The dialog now shows the failure with a Retry button and cannot be saved until the configuration has been read. A refused save (invalid settings, a webhook, Spring or post-action URL on a loopback or link-local address) is shown next to the form with the backend message.

Security & Dependency Updates

  • Secret Rotation Post Actions Refuse Local URLs — The post actions of a secret's rotation configuration (POST /api/v1/rotation/configs, PUT /api/v1/rotation/configs/{id}, PUT /api/v1/secrets/{id}/rotation-config: the secret's Rotation tab) were stored unchecked, although MazeVault calls their webhook and API notification url, Spring actuator_url and endpoints and agent_url after a rotation, so a holder of secret:rotate could make the server send the rotated value to itself, the cloud metadata service or another link-local address. Such a URL (loopback, link-local, unspecified or multicast, in any encoding, localhost) is now refused with 400 naming the field (for example post_actions[0].config.url: URL host "169.254.169.254" is not allowed (internal network)), and nothing is saved. Private ranges stay allowed.
  • Entra Rotation Settings Refuse Local Post-Rotation URLs — The URLs MazeVault calls after an Entra credential rotation (webhook_urls, spring_endpoints and the url, actuator_url, endpoints and agent_url of post_rotation_actions in POST and PUT /api/v1/entra/credentials/{id}/rotation-config) were stored unchecked, so a holder of entra:write could make the server send the new credential to itself, the cloud metadata service or another link-local address. Such a URL (loopback, link-local, unspecified or multicast, in any encoding, localhost) is now refused with 400 naming the field (for example webhook_urls[0]: URL host "169.254.169.254" is not allowed (internal network)), and nothing is saved. Private ranges (10.*, 192.168.*) stay allowed.
  • Certificate Rotation Post Actions Refuse Local URLs — The post-rotation actions of a certificate's rotation configuration (PUT /api/v1/certificates/{id}/rotation/config) now get the same URL check as the secret and Entra rotation settings: a webhook or API notification url, a Spring actuator_url or endpoints, or an agent_url that names the server itself, a link-local (cloud metadata), unspecified or multicast address in any encoding answers 400 with the field (post_rotation_actions[<n>].config.<key>) and nothing is stored. Private ranges stay allowed.
  • Creating a Staged Entra Secondary No Longer Returns the New Secret — POST /api/v1/entra/credentials/{id}/staged-secondary answered the new client secret's value in plain text (new_secret) to the caller, although MazeVault deploys it to the configured post-rotation targets itself and the rotate route never answered it. The staged secondary and the promotion (POST …/promote-primary) now answer only the new credential's id, the key ids, the status and the message; new_secret is gone from both answers.
  • The Entra Credential Routes Are Judged Where the Credential Lives — The routes of an Entra credential (DELETE /api/v1/entra/credentials/{id}, POST …/preflight, POST …/rotate, POST …/dry-run, GET …/rotation-history, GET, POST and PUT …/rotation-config, POST …/staged-secondary, POST …/promote-primary, POST …/verify-secondary), POST /api/v1/entra/sync-conflicts/{id}/resolve and GET /api/v1/entra/subjects/{id} judged their permission only in the environment of the request, so a role limited to dev could delete or rotate a prod credential in Entra, change its rotation configuration or promote its staged secondary, and a role limited to an organization could act on another organization's credentials. They are now judged in the environment of the credential's (the conflict's, the subject's) Entra integration and in its project: 403 otherwise, nothing changed. A role limited to a project now reaches the credential routes of its project's apps; the conflict resolve and the subject read still refuse such a role. An unknown credential answers 404 Entra credential not found before anything is read (before: various 404 texts, 200 with a failed check for the dry run, 409 for the staged secondary and the promotion, 500 for PUT …/rotation-config); an unknown conflict answers 404 conflict not found (before: 500).
  • Security: An Entra Credential Without Its Integration Answers 404 — A credential whose app registration's integration row was missing was judged in the environment of the request, so a role limited to that environment could act on it. Its routes (/api/v1/entra/credentials/{id}/… and POST /api/v1/rotation/resources/entra_credential/{id}/trigger) now answer 404 (fail closed) until the app is repaired.
  • An Entra Integration's Sync Conflicts Are Judged Where the Integration Lives — GET /api/v1/entra/{integrationId}/sync-conflicts judged entra:read only in the environment of the request, so a role limited to dev could list the sync conflicts of a prod integration of its project. It is now judged in the environment and project of the integration: 403 otherwise. An unknown integration answers 404 integration not found (before: 200 []).
  • The Cross-Project Integration List Shows Only What the Caller May Read — GET /api/v1/integrations (the Entra dashboard's integration picker) listed the integrations of every project of every organization, in every environment, to anyone holding integration:read. It now lists only the integrations of the projects of the request's organization in the environments and projects where the caller holds integration:read: a role limited to dev sees the dev integrations, and an organization administrator never sees another organization's. The integrations' configuration was and is never included.
  • The Entra App Routes Are Judged Where the Integration Lives — The app registration routes of an Entra integration (GET and POST /api/v1/entra/{integrationId}/apps, POST …/sync, GET …/subjects) and of an app (GET, PUT and DELETE /api/v1/entra/apps/{id}, GET and POST …/credentials, POST …/mark-managed, GET …/rotation-history) judged entra:read / entra:write only in the environment of the request, so a role limited to dev could read, change or delete the apps of a prod integration, and a role limited to an organization those of another organization. They are now judged in the environment and project of the integration (an app in its integration's): 403 otherwise, nothing changed. An unknown integration answers 404 integration not found (before: 500 to the list, create and sync, and 200 [] to the subjects). A role limited to a project now reaches the Entra routes of its project's integrations and apps, including GET and PUT /api/v1/entra/{integrationId}/scope, which answered such a role 403.
  • A Gateway Reaches Only Its Own Tasks — Any authenticated gateway could list and claim another gateway's tasks by sending that gateway's id (GET /api/v1/gateway/tasks?gateway_id=, GET /api/v1/gateways/{id}/tasks, the gateway_id of POST /api/v1/gateway/tasks/{id}/claim) and could update any task by its id (PATCH /api/v1/gateway/tasks/{id}); task payloads carry Key Vault and Entra operations. A gateway_id other than the authenticated gateway's (X-Gateway-ID) now answers 403 {"error": "gateway_id does not match the authenticated gateway"}, and updating another gateway's task, or an unknown task, answers 404 {"error": "task not found"} (an unknown task answered 500 before). Gateways send their own id, so they are not affected.
  • A Rotation Dry Run No Longer Rotates Anything — A certificate dry run (the Dry Run button of a certificate's Rotation tab, POST /certificates/{id}/rotation/trigger or POST /rotation/resources/certificate_renewal/{id}/trigger with "dry_run": true) ignored dry_run and performed a real certificate renewal, which at a subscription CA could place a paid order, and the console then said that nothing was changed. The resource trigger did the same for every other kind: a dry run of a certificate deployment target synced it, of an Entra credential or a service identity minted a new secret, of a system key or an SSH key pair rotated it. A dry run of these kinds now starts nothing: it answers 200 with the read-only preflight of the resource (RotationPreflightResponse, the same evaluation as the publish gate's preflight), is judged in the resource's own scope with the same permission as the trigger, and is audited with outcome=dry run. A dry run of a deployment target that cannot be synced manually answers 400, as its trigger does. The certificate trigger documents both answers (the execution, or the preflight of a dry run). A secret's dry run still runs its rotation steps in check-only mode, but several of them ignored dry_run and wrote: the secret step stored the preview password ***dry-run-preview*** as the secret's new value (or bumped its version in orchestrator mode), the Key Vault push, agent sync and CI/CD variable sync steps pushed that value to the linked targets, and the Key Vault pull overwrote the local secret with a newer vault value. Each of these steps now skips its write on a dry run. A secret dry run also saved its dry-run workflow into the secret's shared rotation configuration, which the background run then reloaded: a dry run and a real rotation triggered at the same time could swap steps (the dry run executing the real rotation, or the real rotation silently doing nothing). A secret dry run now keeps its workflow to itself and never writes the rotation configuration; it no longer takes a backup, restores or compensates anything on failure, runs post-rotation actions, updates integration sync status, moves the rotation schedule or resolves a rotation-failure incident, and it no longer fails with "rotation already in progress" while a real rotation runs. Its execution is marked dry_run in its metadata, and so is the queued execution record that the trigger answers 202 with, so resuming a failed one no longer moves the schedule or resolves an incident.
  • An Agent Integration Is Edited Only Where It Lives — PUT /api/v1/agent-integrations/{id} judged the integration id as if it were a project id, which belongs to no project, so a project:write role limited to the request's organization edited the agent integrations of every other organization, and one limited to dev edited a prod integration (or moved one to prod) by naming dev. The integration is now judged in its own project, in its own environment and in the environment the request binds it to; a role assigned to one project edits only that project's integrations.
  • A Secret Is Linked Only to an Integration of Its Own Project, and Its Links and Targets Are Judged Within It — POST /secrets/{id}/links accepted the integration of any project or organization (only the environment was compared) and then pushed the secret value to it, so a user who could rotate one secret could send its value to an integration they had no access to. The integration must belong to the secret's project (otherwise 404). DELETE /secrets/{id}/links/{linkId} deleted any link whatever the secret in the path; the link must belong to the secret (otherwise 404, also for an unknown link, which answered 204 before). A deployment target that names an integration of another project (binding_ref or config.integration_id) now also needs secret:write in that project.
  • The Rotation Config, Links and Targets of a Secret Are Judged in the Secret's Own Environment and Project — /secrets/{id}/rotation-config, /links and /rotation/targets (and their sync) judged a request in the environment the client named (?environment= or the header), so a user limited to dev could read, change, link or sync the rotation of a prod secret by naming dev. They are now judged in the environment and project of the secret itself; a role assigned to one project works on the secrets of that project and is refused on the others. DELETE /secrets/{id}/links/{linkId} with an invalid secret id now answers 400 instead of deleting the link.
  • The unauthenticated agent enrollment routes are removed from the core API — POST /api/v1/enroll, POST /api/v1/heartbeat and POST /api/v1/key-exchange of the backend no longer exist. Enrollment needed no authentication and returned license keys, wrapped cipher keys and OAuth clients to any caller; the key exchange could never succeed. No MazeVault component called them: licensing and POC enrollment run through the license server (LICENSE_SERVER_URL), which is unaffected.
  • ACME finalize and revoke-cert no longer send CA provider text — When the CA provider failed or refused to sign or revoke, the ACME problem document carried the provider's error text. Every ACME server error (HTTP 5xx) now answers the fixed Internal server error with type serverInternal; 4xx answers keep their type and detail.
  • SCEP, OCSP and ACME failures no longer send internal error text — A failed SCEP operation, OCSP request or ACME server call now answers a fixed Internal server error (status codes and the ACME problem document shape are unchanged); the cause is written to the server log only. Before, the answer could carry database or library text.
  • Rotation Configs, Executions, Backups and Resources Are Judged in the Environment and Project of the Object Itself — The rotation routes judged a request in the environment the client named (?environment= or the header) and, for configs and backups, in no project, so a user limited to dev could read, change, delete or preflight the rotation config of a prod secret by naming dev, and a user whose role was assigned to one project was refused on that project's configs. Now /rotation/configs/{id} (and /preflight), POST /rotation/configs (the secret in the body), /rotation/executions/{id} (and /resume), /rotation/backups/{id}/reveal|restore|acknowledge and /rotation/resources/{kind}/{id} (and /readiness, /history) are judged in the project and environment of the secret, execution or resource itself. Reveal, restore and the previously unchecked acknowledge of a backup need secret:read on the backup's secret in its own project and environment. The lists of a project (rotation-configs, rotation-resources) drop the environments the caller may not read, and /rotation/executions, /rotation/backups and /rotation/resources list only the request's organization (and the readable environments).
  • Rotation Triggers Are Judged in the Environment and Project of the Rotated Resource — The four rotation triggers (POST /certificates/{id}/rotation/trigger, POST /rotation/resources/{kind}/{id}/trigger, POST /rotation/configs/{id}/trigger, POST /secrets/{id}/rotate) judged the permission in the environment the client named, so a user who may run rotations in dev only could renew a prod certificate or rotate a prod secret by sending ?environment=dev. Every trigger, and GET /rotation/resources/{kind}/{id} with its /readiness and /history, is now judged in the resource's own scope: a secret in its secret's environment and project, a certificate renewal in the certificate's, a deployment target in its certificate's, an Entra credential in its app registration's project (and its integration's environment); an SSH key pair needs its owner or ssh:admin; a system key or a service identity, which belong to no project, need a role that is limited to neither a project nor an organization. A role assigned to one project can now trigger the secrets and certificates of that project. An unknown resource answers 404 (the unknown secret of /secrets/{id}/rotate and the unknown certificate of /certificates/{id}/rotation/trigger answered 500 before).
  • Rotating a System Key Requires system:admin — POST /rotation/resources/system_key/{id}/trigger was allowed to anyone with rotation:write, while POST /admin/rotation/rotate requires system:admin. It now also requires system:admin, not limited to an organization or project; without it the trigger answers 403. The console shows the System Keys tab only with system:admin.
  • Rotation Rows of No Project Need a Role Not Limited to an Organization — Rotation executions without a project and the rotation resources of no project (system keys, SSH key pairs, service identities and tokens without a project) were listed by GET /rotation/executions and GET /rotation/resources, and readable by id, for any role that passed the route, including an administrator limited to one organization (or to another organization). They are now listed and read only by a role limited to neither a project nor an organization; an organization administrator sees the rows of its organization's projects. This covers the rotation routes (/rotation/...); the service identity routes themselves are covered by "Service Identities Need a Role Not Limited to an Organization".
  • SSH Private Key Export Is Judged Where the Key Pair Lives — POST /api/v1/ssh/keys/{id}/export checked ssh:admin in the fixed dev environment and ignored the key pair's project and organization, so an ssh:admin limited to one organization exported the private keys of another organization's key pairs. The export now needs ssh:admin on the key pair's project in the request's environment; a key pair of no project needs ssh:admin limited to neither a project nor an organization (403 otherwise, still audited as denied). An unknown key pair answers 404 before the permission check.
  • Rotation Conflicts and Resume Failures Carry Fixed Messages — Several 409 answers of the rotation routes (resume, backup acknowledge, secret and certificate triggers, system key rotation) and the audit entry of a failed resume repeated the internal error text, which could carry execution ids, states or a provider's wrapped error. They now carry fixed messages, for example rotation execution is already completed and rotation backup is not in recovery_required state (no longer followed by the current state); a certificate renewal already waiting answers renewal is waiting on an external party with the code RENEWAL_IN_PROGRESS.
  • Certificates of No Project Need an Unlimited Role on the Rotation Routes — GET /rotation/resources already hid the renewal and deployment rotation of certificates of no project (imported or platform certificates) from roles limited to an organization, but the by-id rotation resource routes, their triggers and /api/v1/certificates/{id}/rotation* and /targets* still served them to such a role. These rotation routes now need a role limited to neither a project nor an organization for a certificate of no project (403 otherwise); the certificate routes themselves (view, export, renew) are unchanged.
  • Retrying the Deployment of a Key Pair of No Project Works for Unlimited Roles — PUT /api/v1/ssh/keys/{id}/deploy answered 404 to everyone, its owner included, for an SSH key pair of no project. Such a key pair is now retried by its owner or an ssh:admin whose ssh:write is limited to neither a project nor an organization; an organization-limited role gets 403.
  • The SSH Administrator Override Is Judged Where the Key Pair Lives — On /api/v1/ssh/keys (generate, import, list, retry deploy) and /api/v1/ssh/authorized-keys (list, deploy, revoke, remove), acting on another user's key pair checked ssh:admin in the fixed dev environment, ignoring the organization and project: an ssh:admin limited to dev acted as an SSH administrator everywhere, prod included, and one limited to prod was none. ssh:admin now counts in the request's environment on the key pair's project, as on the SSH rotation routes; a key pair of no project and the lists across all owners need ssh:admin limited to neither a project nor an organization (a list filtered by project: ssh:admin on that project). Generating or importing a key pair for another user now validates the project before this check, so an unknown project answers 404 first.
  • The Admin System Key Routes Need an Unlimited System Administrator — GET /api/v1/admin/rotation/keys and POST /api/v1/admin/rotation/rotate accepted system:admin limited to one organization, while rotating the same installation-wide keys through /rotation/resources/system_key/{id}/trigger needed system:admin limited to neither a project nor an organization. Both admin routes now need that unlimited system:admin too (403 otherwise).
  • Service Identities Need a Role Not Limited to an Organization — Service identities belong to no organization, yet GET, POST, PUT and DELETE /api/v1/service-identities (and /{id}) let a role limited to one organization list, create, edit and delete the service identities of the whole installation. These routes now need their service_identity:* permission through a role limited to neither a project nor an organization (403 otherwise), as the rotation of a service identity secret already did.
  • Refused Rotation Resource Reads No Longer Write the Read Model — GET /api/v1/rotation/resources/{kind}/{id}, /readiness and /history created or refreshed the rotation resource row before they checked the caller, so a caller without access (or of another organization) made the server write, and a project-less SSH key pair answered 500 instead of 403. The caller is now judged first; a refused read writes nothing and answers 403, an unknown object 404 as before.
  • A Rotation Preflight Is Read Only Where Its Object May Be Read — GET /api/v1/rotation/preflights/{id} returned any preflight to anyone with rotation:read in the request's organization, including preflights of another organization's secrets and certificates and of system keys. The preflight is now judged like the readiness of the object it checked: rotation:read must hold in that object's environment and project (403 otherwise), a preflight of a system key, service identity or of no object needs a role limited to neither a project nor an organization, and a preflight whose object no longer exists answers 404.
  • Legacy Agent Routes No Longer Hand Out Agent Tokens or Other Agents' SSH Keys — Any authenticated caller (a user without permissions, an API token) could call POST /agent/discover with the hostname of a registered agent and receive a valid token of that agent, or with a new hostname to create an agent, and could read and confirm the pending authorized SSH keys of any agent through GET /agent/{agent_id}/authorized-keys/pending and POST /agent/{agent_id}/authorized-keys/{authorized_key_id}/confirm. The two /agent/{agent_id}/... routes now serve only the active agent named by agent_id itself (anyone else gets 403 agent is not registered, an invalid agent_id included, which was 400; an inactive or revoked agent gets 403 agent is not active even on its own path), and POST /agent/discover needs the agent:admin permission not limited to a project or an organization (403 otherwise). Agents keep using /api/v1/agents/me/authorized-keys/..., which are unchanged.
  • Refused and Unknown SSH Private Key Exports Are Audited — Only a successful POST /api/v1/ssh/keys/{id}/export left an ssh_key.export audit entry. A refused export that reaches the handler (not the owner of the key) and an export of an unknown key now leave one too, with their outcome (never any key material). A request the route guard already refuses with 403 (no ssh:admin) is not audited here.
  • SSH Key Generation, Import, Create-and-Deploy and Signing Are Judged in the Caller's Organization — The project_id in the body of POST /api/v1/ssh/keys, /ssh/keys/import, /ssh/keys/create-and-deploy and /ssh/sign was not checked, so an administrator limited to one organization could create key pairs and sign certificates in another organization's project. Such a project, like an unknown one, now answers 404; the permission must also hold on the project in the request's environment (403).
  • SSH Key Pairs Are Created for Another User Only by an SSH Administrator, and Key Creation, Deployment and Signing Are Audited — POST /api/v1/ssh/keys and POST /api/v1/ssh/keys/import took the owner from the user_id in the body, so any ssh:write holder could create key pairs in another user's name (and the audit entry named that user). They now answer 403 unless the user_id is the caller's or the caller holds ssh:admin, and the audit entry names the caller. Create-and-deploy, authorized key deployment requests and failed retries now leave audit entries with their outcome.
  • Retrying an SSH Key Deployment Is Limited to the Key's Owner or an SSH Administrator — PUT /api/v1/ssh/keys/{id}/deploy pushed any user's public key to an agent for anyone holding ssh:write. It now applies the rule of the other key pair routes: the owner of the key pair or a holder of ssh:admin may retry (everyone else gets 403), and a key pair of a project of another organization answers 404 as an unknown one does.
  • Resolving an Incident Is Judged in the Incident's Own Organization, Project and Environment, and Is Audited — POST /api/v1/incidents/{id}/resolve resolved any organization's incident for anyone holding incident:resolve, answered 200 for an id that does not exist and left no audit entry. It now answers 404 for an unknown incident and for another organization's one, a role limited to a project resolves only the incidents of that project (none without a project), the permission is also checked in the incident's environment, and every resolution is written to the audit log with its outcome. A failure no longer returns the internal error text.
  • Rotation Problems: Another Organization's Failures Are Not Listed or Acknowledged — The Rotation Problems list returned the failed rotations of every organization and acknowledging a failure worked on any organization's row. The list now holds only the failures of the caller's organization and acknowledging another organization's failure answers 404, as for an unknown id. The platform-wide failures that belong to no project are listed and acknowledged only for a role that is not limited to an organization.
  • A PKCS#11 Module Path Is Loaded Exactly as the Operator Listed It — The HSM_PKCS11_MODULES check compared the lexically cleaned path of a device, while the library was loaded from the path as written; the operating system resolves .. through symbolic links, so /allowed/dir/link/../lib.so passed the check and could load a different file. A module_path whose cleaned form differs from what was entered (.., ., doubled or trailing slashes) is now refused; use the path exactly as listed.
  • An EAB Credential Can Be Revoked Only in Its Own Organization, and a New One Only for Its Own Projects and Templates — DELETE /api/v1/organizations/{id}/acme-credentials/{credId} looked the credential up by its id alone, so an administrator of one organization could revoke the ACME EAB credentials of another (the ACME clients of that organization then failed to register). The credential is now looked up in the organization of the path (404 otherwise), revoked by a conditional update (a second revoke answers 409 and racing revokes revoke once). Creating a credential (POST .../acme-credentials) accepted the project and the default template of any organization; both must belong to the organization of the path (404 otherwise).
  • The Organization's PKI Settings Cannot Link Another Organization's CA — Saving an organization with pki_mode: root and a pki_root_cn found the root CA by name among the CAs of all organizations and linked the first one no live account used, so an organization administrator could name the CA of another organization (a CA's name is the issuer of every certificate it issues) and issue from it, bypassing the ca_authority_id rule. Only the organization's own CAs are found now (deleted accounts counted); without one a new root is created.
  • A PKCS#11 Module Path Can No Longer Load Arbitrary Native Code — The module_path of a PKCS#11 HSM device is a shared library the server loads, and loading runs the library's constructors, so anyone allowed to write an HSM device (an organization administrator, an API token) could run native code in the process that holds the master key. Only the modules the operator lists in HSM_PKCS11_MODULES are loaded, at the single load point (connection test, health monitor, key routing, existing devices); creating or changing a PKCS#11 device with another module answers 400, and so does POST /api/v1/ca/{id}/hsm with a PKCS#11 provider whose module is not listed or missing.
  • PKCS#11 HSM Sessions Are Serialised and Closed Safely — The PKCS#11 provider shared one HSM session and an unguarded key-handle map between concurrent signing calls, so parallel signs could crash the server (concurrent map write) or use the session of a provider that was being closed. One lock now guards the session and the cache; an operation on a closed provider answers an error and never touches the released module.
  • A Failed CA Sync No Longer Stores or Logs What the CA Said — When a sync (the Sync button, the scheduler, the product sync) failed, the raw provider error — which can carry the response body of the CA, the name of an internal host or an echo of the credential it refused — was stored in the account's validation_error and the sync job, readable through the account routes, and written to the log. They now carry a fixed text with the HTTP status the CA answered with; the cause is logged without the credentials and without the response body.
  • An Organization's CA Cannot Be Claimed After Its Account Is Deleted — Linking a CA account to a CA (ca_authority_id in the credentials) accepted any organization-level CA that no live account of another organization used, so after organization B deleted its internal CA account, organization A could link B's CA (whose id is part of every certificate it issued) and issue from it. An organization-level CA now has to be linked by an account of the same organization and by no account of another one, deleted accounts counted; a CA no account links is refused.
  • Changing the URL of a CA Account Requires Its Credentials — An update that changed base_url (or another URL the platform calls with the account's credentials) without sending the credentials skipped the connection check, and the next sync or product sync then sent the stored API key to the new host, so an administrator who never knew the key could collect it. Such an update now answers 400; send the credentials with it and they are checked against the new host.
  • CA Account Secrets in the Settings Are Masked and Encrypted — The settings a CA provider's schema marks secret (the DNS provider credentials of SSLmarket's DNS validation, the EAB HMAC key of the ACME providers) were stored as plain JSON and returned in metadata by every CA account route to everyone who may read CA accounts. They are now encrypted at rest (ENC: values; existing plaintext is encrypted on the account's next change) and shown as __MASKED__. An update that sends the mask or an empty value back keeps the stored secret, a new value replaces it and null removes it, so editing the account in the UI or applying it with Terraform no longer overwrites or exposes them.
  • Certificate Import Is Refused Without an Organization — The import matches an existing certificate only among the organization's own; a request that carried no organization silently matched certificates of any organization. It now answers 403 "organization context is required", as adoption does.
  • HSM Devices and Renewal Policies Are Looked Up in the Organization of the Path — The device and policy routes under /api/v1/organizations/{id}/hsm-devices and /renewal-policies read, changed, tested or deleted a device or policy by its id alone, whatever organization the path named, so an administrator of one organization (or any holder of the permission) could act on another organization's devices and policies. They are now looked up in the organization of the path: another organization's id answers 404 and changes nothing. Updates and deletes lock the row, so a concurrent delete is not undone. Deleting an unknown renewal policy now answers 404 instead of 204, and an invalid organization id in the path of a device get, test or health answers 400.
  • Certificate Import Errors Show Only Input Errors, Whatever Words the Cause Contains — The import routes decided whether to show an error by looking for words such as "invalid" or "required" anywhere in its text, so a key storage, vault or database failure whose message contained one of them (for example a failed key offload to a vault, or an invalid byte sequence in an update) answered 400 with the internal text, in the single import and in the item errors of bulk and file imports. An error is now shown only when it starts like one of the import service's own input errors (invalid or unparsable certificate or key, mismatch, duplicate, unsupported format, wrong password, missing permission); everything else answers 500 "Internal server error" or, in an item, "Failed to import certificate", and the cause goes to the log.
  • Certificates Issued From a Request Land in the Environment of the Request — POST /api/v1/certificates/csr is judged in the environment of the call, but the certificate issued from it was always created in prod, so a user limited to dev could obtain a prod certificate through a template that needs no approval. The request now stores its environment (migration 000218, existing requests keep prod) and the certificate takes it, whether it is issued at once or when an approver approves it (the approver's own environment does not matter). Behaviour change: certificates requested through POST /api/v1/certificates/csr now appear in the selected environment, and a client that sends no environment gets the server's default one, not always prod. POST /api/v1/projects/{id}/csr takes the environment of its call the same way; POST /api/v1/gateways/csr takes the environment its gateway is bound to.
  • A Certificate Import Cannot Change Another Organization's Certificate — Importing a certificate or a private key completes the body of, or attaches the key to, the existing certificate with the same thumbprint, but the lookup spanned organizations: an administrator of one organization importing a public certificate without a project could complete or take custody of another organization's record. The match is now limited to the caller's organization (its projects' certificates, and project-less ones that no other organization's CA account issued); a certificate of another organization is no match, so the import creates its own and leaves the other record, its project and its key alone.
  • Rejecting or Archiving a Certificate Request Can No Longer Destroy the Key of One Being Issued — Reject and archive deleted the externally stored private key of a pending request (Azure Key Vault, secret manager) before they changed its state, so an approval that arrived in between lost its key while the reject answered 409, and archiving had no state condition at all. Both now claim the request first with a conditional change and delete the key only afterwards (a failed key deletion undoes the claim). Archiving a request that is approved, being issued or handed to the CA now answers 409 "certificate request is being issued" and changes nothing.
  • Adopting a Discovered Certificate No Longer Touches a Managed Certificate of Another Environment or Organization — Adoption links an existing managed certificate with the same thumbprint, fills its empty fields, moves it into the target project when it had none and returns it, but the match ignored environment and organization: a user limited to dev who adopted in dev could move, fill and read a prod certificate, and a project-less certificate of another organization was claimed. The match is now limited to certificates of the caller's organization (never system certificates), prefers the one in the target project and environment (among several certificates with the same thumbprint, such as a dev and a prod copy, adopting in dev takes the dev one, and a certificate without a project never outranks the one in the target project), and a match in another environment answers 409 "matching managed certificate is in another environment" with nothing changed (bulk adopt counts it per certificate). Assigning discovered certificates to a project that is not a certificate project now answers 400, as adoption does.
  • CA Account Sync Interval and Renewal Lead Are Validated — Connecting or updating a CA account accepted a sync interval of zero or less, which made the scheduler sync the account (and call the CA) on every tick, and a negative renewal lead. sync_interval_minutes must now be at least 1 and renewal_lead_days not negative; otherwise 400.
  • A CA Account Can Link Only a CA of Its Own Organization — The ca_authority_id of the credentials of an internal or ACME CA account (connect and update) decides which CA issues from the account, but any existing CA ID was accepted, including a CA of another organization's project. It must now name a CA of one of the organization's projects or an organization-level CA that no other organization's account uses; anything else, and an unknown ID, answers 400 "ca_authority_id does not name a CA of this organization" and changes nothing.
  • Legacy CA Account Routes Stay in the Caller's Organization — GET /api/v1/ca-accounts/{id}/products and POST /api/v1/ca-accounts/{id}/sync took the account by its ID alone, so a holder of ca:read or ca:admin could read the products of, or trigger a product sync for, an account of another organization (an unknown ID answered an empty list or 500). They now answer 404 unless the account belongs to the organization of the request, as the organization-scoped routes always did; the sync is audited only when it ran.
  • CA Provider Schemas Are Readable With a Project-Scoped Role — GET /api/v1/ca-providers/schemas and /api/v1/ca-providers/{type}/schema hold only static descriptions of the supported CAs, but a role assigned to a single project was refused (403) even with ca:read, so the certificate request form could not load them for such users. The two routes now accept ca:read held in any project; the CA accounts themselves stay organization-wide.
  • Importing a Key for a Discovered Certificate Is Judged in the Certificate's Environment — POST /api/v1/discovered-certificates/{id}/import-key replaces the private key of the managed certificate but was judged only in the environment of the request, so an organization-wide user limited to dev could replace the key of a prod certificate. It now needs discovered_cert:write in the environment and project of the managed certificate (403 otherwise, key unchanged). The bulk assign, bulk adopt and bulk ignore descriptions now say that every certificate is checked before any is changed (the changes themselves are applied one by one).
  • Adopting Discovered Certificates Works With API Tokens — Adopting (POST /api/v1/discovered-certificates/{id}/adopt) and bulk adopting (/bulk/adopt) answered 403 "organization context is required" for an API token even when it held the permission, because the routes did not see the token's default organization, while assigning worked. Adoption now uses the same organization as every other route (a session's, or the default organization for a token). The refusal for a target project or environment the caller may not act in now reads insufficient permissions for environment (it was access denied); sessions and JWTs are otherwise unchanged.
  • Storage Errors of a Discovered Certificate's Key Import Stay Out of the Answer — Importing a private key for an adopted discovered certificate (POST /api/v1/discovered-certificates/{id}/import-key) answered 400 with the cause when the key could not be stored: no key offload service in orchestrator mode, a secret manager that refused the key (its message can name hosts and paths), or a failed encryption. These are server-side failures and now answer 500 with the generic Internal server error; the cause goes to the server log with the request ID. A key that cannot be read or does not match the certificate still answers 400 with the reason.
  • Certificate Request Decisions Are Audited — Submitting, approving, rejecting and archiving a certificate signing request (/api/v1/certificates/csr) wrote no entry to the audit log. Each successful change now records certificate_request_submitted, certificate_request_approved, certificate_request_rejected or certificate_request_archived with the actor, the request and its project (the rejection reason is part of the details); refused calls write nothing, and no entry holds key material.
  • Certificate Requests and Imports Are Judged in Their Project and Environment — A role assigned on one project could neither submit a certificate request nor import a certificate there, and an API token or session limited to an environment could import into any environment. POST /api/v1/certificates/csr is now judged in the project of its body (without one, only a role that is not limited to projects may submit an unbound request), and the import routes (/api/v1/certificates/import, /import/bulk, /import/file) in the project and environment they name (no environment means prod, no project needs an organization-wide role); a bulk import is refused whole when one item is out of scope. Importing a certificate that already exists changes it only for a caller who may import in that certificate's own project and environment (others get 403, not its details). The project, template, CA account and key storage integration of a request or import must belong to the caller's organization and, for the integration, to the project (404 otherwise). The request queue (list, statistics, approve, reject, archive) stays organization-wide.
  • Certificate Requests Use the Caller's Organization, Also for API Tokens — Submitting a certificate request (POST /api/v1/certificates/csr) took the organization and the agent from the request body when the caller had no organization of its own, which is the case for API tokens (Terraform, SDK): a token's request was stored without an organization or in one the client named, and approving, rejecting or archiving a request through a token always failed. The organization is now always the caller's (a session's, or the default organization for an API token), the agent is only set for an agent's own token, and the list, statistics, approve, reject and archive routes judge requests in the same organization, so tokens can use them and a token no longer lists requests of every organization.
  • Discovered Certificate Assignments and Adoptions Are Checked in Their Target Project and Environment — Assigning discovered certificates to a project (POST /api/v1/discovered-certificates/{id}/assign and /bulk/assign) did not look at the project at all: an unknown project failed with a database error, and a project of another organization or an archived one was accepted, which also moved the linked managed certificate there. The target must now be a live project of the request's organization (404 target certificate project not found otherwise), and discovered_cert:assign must hold in it, in the request's environment and in the environment of every managed certificate that moves with the discovered one (403 insufficient permissions for environment), so a user who may assign in dev only cannot move a prod certificate. Adoption (/adopt, /bulk/adopt) is judged in the project and environment of its body by the same check as every other route. The discovered certificate and archived certificate routes remain organization-wide: a role assigned on one project does not give access to any of them, with an API token, a web interface session or a JWT.
  • An SRP Session Allows One Proof — POST /api/v1/auth/srp/verify kept the session after a wrong client proof, so the proof could be tried again until the session expired (two minutes). A session now ends with the first proof, right or wrong; a client that fails must start again with POST /api/v1/auth/srp/init.
  • Universal Auth Login Reveals Nothing About a Secret to an Untrusted Address — POST /api/v1/auth/universal-auth/login checked the client secret first and the trusted addresses afterwards, so a request from an address outside the trusted list answered 403 when the secret was valid and 401 when it was not — anyone could use the answer to test secrets. The trusted addresses are now checked first, and an untrusted address gets the same 401 Invalid credentials as a wrong secret or an unknown client (the secret is not even checked); the reason (client id and address) is only written to the server log. Operators who see 401 from a client with correct credentials should check the trusted addresses of its configuration.
  • Universal Auth Changes Are Audited — Attaching, updating and revoking Universal Auth for a service identity (/api/v1/auth/universal-auth/identities/{id}) and creating and revoking its client secrets wrote no audit entry. Each change now records ATTACH_UNIVERSAL_AUTH, UPDATE_UNIVERSAL_AUTH, REVOKE_UNIVERSAL_AUTH, CREATE_CLIENT_SECRET or REVOKE_CLIENT_SECRET with the calling user and address; the details name the service identity, the changed fields and the secret's description, never a secret.
  • LDAP Sign-In No Longer Logs the Username — The LDAP login (POST /api/v1/auth/ldap/login and the console LDAP sign-in) logged the user-search filter with the entered username filled in, also when the search failed, and logged the username again when no single user matched. The log now records the filter template only, as the connection test does, and no longer the username.
  • SSO Logins Are Rate Limited and Clean Up Expired OAuth States — Every unauthenticated hit on GET /api/v1/auth/sso/{entra,github,gitlab}/login stored an OAuth state row that was never removed once it expired. Each login now deletes the expired states first, and the three login routes are rate limited per client IP (30 per second, burst 60, like /auth/sso/spnego/login); above the limit they answer 429 (Too many requests).
  • Database Log Lines No Longer Contain Query Values — When a database statement failed or was slow, the server log printed it with every value filled in, including single-use SSO exchange codes and the tokens stored with them. The log now shows the statement with placeholders only.
  • Entra ID Sign-In Without Configuration Returns to the Login Page — GET /api/v1/auth/sso/entra/login answered a raw JSON 500 (Failed to initiate SSO login) when no redirect URI was configured, so the browser showed an error page. It now redirects (302) to the console login page with ?error=sso_not_configured (any other failure: ?error=sso_failed, logged), using FRONTEND_URL or the host the request came from, like the callback.
  • SSO Callbacks Report Fixed Error Codes — A failed Entra ID, GitHub or GitLab callback put the internal error text into the redirect (/login?error=<message>), which could carry identity-provider or server details into the browser history and logs. The error parameter is now one of missing_auth_code, missing_code_or_state, invalid_state (unknown or expired OAuth state) and sso_failed; the detail is logged on the server only.
  • SSO Sign-In Never Puts the Access Token in the URL — When the single-use exchange code of an Entra ID, GitHub or GitLab sign-in could not be stored, the callback (GET /api/v1/auth/sso/{entra,github,gitlab}/callback) fell back to redirecting to /login?access_token=<JWT> and setting the refresh token cookie, which exposed the token in the browser history, proxy logs and the Referer header. The sign-in now fails instead: it redirects to /login?error=sso_failed without any token or cookie; the cause is logged.
  • The LDAP Connection Test No Longer Logs the Test Username — POST /api/v1/auth/sso/ldap/test wrote the optional test_username to the server log, also inside the logged search filter. The log now records only whether a test username was given and the filter template.
  • Identity Provider and SPNEGO Changes Are Audited — Creating, updating and deleting an identity provider (/api/v1/identity-providers) and saving the SPNEGO/Kerberos configuration (POST /api/v1/auth/sso/spnego/config) wrote no audit entry. They now record CREATE_IDENTITY_PROVIDER, UPDATE_IDENTITY_PROVIDER, DELETE_IDENTITY_PROVIDER and CONFIGURE_SPNEGO (entity IDENTITY_PROVIDER) with the acting user and IP; the details name the provider and its type, or the service principal and realm, never a secret.
  • An LDAP Bind Password That Cannot Be Encrypted Is No Longer Stored in Plaintext — When the bind password of an LDAP identity provider could not be encrypted, POST /api/v1/identity-providers and PUT /api/v1/identity-providers/{id} silently stored it in plaintext. They now answer 500 (Internal server error) and store nothing; the cause is logged.
  • Group Search of an Identity Provider Could Alter the Graph Filter — GET /api/v1/identity-providers/{id}/groups?search=… put the search text into a quoted Microsoft Graph $filter without escaping single quotes, so a search containing ' could end the string and add its own filter conditions. Single quotes are now doubled, as OData string literals require, so the search is always matched as a display-name prefix.
  • Identity Provider and LDAP Config Answers No Longer Return Stored Secrets — POST /api/v1/identity-providers and PUT /api/v1/identity-providers/{id} answered the stored config in full, so even an update that changed only the name returned the Entra ID client_secret in clear (and the encrypted LDAP bind_password). They now mask client_secret, bind_password, api_token and token like the list and detail routes (**** plus the last four characters). POST /api/v1/auth/sso/ldap/config now answers the bind password as __MASKED__, as GET does.
  • Web Interface Security Headers (OWASP ZAP findings) — Every web interface response now carries Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy, and static assets now also get X-Content-Type-Options: nosniff (nginx dropped it there). Static assets no longer send Access-Control-Allow-Origin: *. A Content Security Policy (built per response, with sha256- hashes of the inline scripts of the application in script-src) and Cross-Origin-Embedder-Policy: require-corp are enforced; they are no longer report-only. Static assets are served pre-compressed (gzip, brotli). Missing files such as favicon.ico or sitemap.xml return 404 instead of the application page, and robots.txt disallows indexing. The deprecated X-XSS-Protection header was removed. The headers are set by the web application itself (the Nuxt server), so they are the same behind the Docker Compose edge and the Helm ingress; the /swagger pages are served by the backend and carry no application CSP.
  • SAML Login No Longer Exposes the Refresh Token to the Browser — The SAML ACS endpoint (POST /api/v1/auth/saml/callback) used to answer a valid assertion with a JSON body containing access_token and refresh_token, readable by browser JavaScript. It now works like the Entra ID, GitHub and GitLab sign-in: it stores a single-use exchange code valid for 60 seconds and redirects (302) to FRONTEND_URL/login?sso_code=… (without FRONTEND_URL, to the host the callback was called on); the console exchanges the code via POST /api/v1/auth/sso/exchange, which returns only the access token and sets the refresh token as an HttpOnly cookie. A replayed or expired code is rejected with 401. The callback is posted by the identity provider through the user's browser and was never consumed by the console, SDKs or the Terraform provider, so no integration change is needed; set FRONTEND_URL if the console is served from a different origin than the API.
  • Go Transitive CVEs Cleared (x/net, x/mod) — golang.org/x/net was upgraded 0.55.0 → 0.56.0 and golang.org/x/mod 0.37.0 → 0.40.0 across the agent, cli, k8s-operator, and terraform-provider-mazevault modules to resolve CVE-2026-46600, CVE-2026-56864, and CVE-2026-56865.
  • Cross-Module Dependency Refresh — Additional module dependencies were updated: golang.org/x/crypto → 0.55.0 and golang.org/x/sys → 0.47.0 in ocsp-responder, cloud.google.com/go and related packages in backend, and google.golang.org/grpc, google.golang.org/genproto, and OpenTelemetry packages in terraform-provider-mazevault.
  • gRPC DoS Cleared (CVE-2026-84445) — google.golang.org/grpc was upgraded 1.83.1 → 1.83.2 in backend and terraform-provider-mazevault to resolve CVE-2026-84445 (gRPC-Go denial of service via malformed RPC requests), which was failing the dev-pipeline Trivy filesystem scan. The fix is pinned to the 1.83.2 patch line (the 1.84.0 line is still affected). A transitive golang.org/x/mod → 0.40.0 bump (CVE-2026-56864 / CVE-2026-56865) that a subsequent tooling update pulled back to 0.39.0 was re-applied. Verified with go build, go vet, govulncheck (0 reachable), and a clean Trivy CRITICAL,HIGH --ignore-unfixed scan.
  • Jackson Core/Databind DoS Cleared (CVE-2026-68497, CVE-2026-89407, CVE-2026-89425, CVE-2026-91776, CVE-2026-91777) — com.fasterxml.jackson.core:jackson-databind was upgraded 2.18.8 → 2.18.11 in the Java SDK (sdks/java/pom.xml). The initial → 2.18.10 bump resolved CVE-2026-68497 (CPU DoS via unbounded numeric parsing); a subsequent Jackson advisory wave then flagged four more HIGH denial-of-service issues — two in transitive jackson-core (CVE-2026-89407 regex backtracking, CVE-2026-89425 unbounded StringBuilder growth) and two in jackson-databind (CVE-2026-91776 unbounded cache growth, CVE-2026-91777 quadratic forward-reference completion) — all fixed by 2.18.11, which also transitively aligns jackson-core → 2.18.11. Everything stays on the 2.18.x patch line (drop-in, no API changes) and the dev-pipeline Trivy filesystem scan is green. The Java SDK now imports the Jackson BOM (com.fasterxml.jackson:jackson-bom, pinned via a single jackson.version property) in dependencyManagement, so jackson-core, jackson-databind, and jackson-annotations stay version-aligned and future Jackson bumps are a one-line change.
  • x/net HTTP/2 DoS Cleared (CVE-2026-78669) — golang.org/x/net was upgraded → 0.60.0 across backend, agent, cli, k8s-operator, and terraform-provider-mazevault to resolve CVE-2026-78669 (net/http2 denial of service via excessive HTTP/2 SETTINGS frames), which was failing the dev-pipeline Trivy filesystem scan. All five modules build and test clean and the Trivy CRITICAL,HIGH --ignore-unfixed scan is green across every module.
  • Second Dependency Wave (verified) — A further batch of safe dependency bumps was applied and verified (backend go build/vet/full go test, agent linux+windows build/test, frontend tsc -b/lint/build/165-test on Node 24, Trivy, govulncheck): backend Go (Azure azcore 1.23.2 / azidentity 1.14.1, azblob 1.8.2, aws-sdk-go-v2/config 1.33.8, service/s3 1.114.2, gin-contrib/cors 1.7.9, go-ldap/ldap/v3 3.4.15, microsoft/go-mssqldb 1.11.2, golang.org/x/crypto 0.57.0, golang.org/x/oauth2 0.37.0, golang.org/x/time 0.16.0, google.golang.org/api 0.301.0 — the latter transitively advanced google.golang.org/grpc → 1.84.0, which the current Trivy database no longer flags for CVE-2026-84445, and a coordinated lockstep bump of k8s.io/api + k8s.io/apimachinery + k8s.io/client-go 0.28.0 → 0.37.1, which the Kubernetes rotation steps compile and test clean against); frontend npm (lucide-react 1.50.0, @types/node 26.6.4, globals 17.13.0, vitest 5.0.3); and GitHub Actions (actions/checkout, actions/setup-go, actions/setup-node v5 → v7, and actions/download-artifact v4 → v8, which stays compatible with the already-pinned actions/upload-artifact@v7 and the repository's name/pattern downloads). Deferred (blocked): the previously tracked typescript 7 and eslint 10 PRs (still blocked by their respective ESLint peer ranges).
  • Consolidated Dependency Bumps — A large batch of routine dependency updates was applied and verified end to end (backend/agent go test, frontend tsc/lint/build/165-test on Node 24, Trivy, govulncheck): backend Go modules (microsoftgraph/msgraph-sdk-go, swaggo/swag, swaggo/gin-swagger, Azure azcore/azblob, Venafi/vcert, aws-sdk-go-v2 core/config/credentials/s3/route53, gin-contrib/cors, jackc/pgx, miekg/pkcs11, lib/pq, mongo-driver, crewjam/saml, prometheus/client_golang, stretchr/testify), agent Go modules (go-resty, prometheus/client_golang, golang.org/x/crypto 0.57.0, golang.org/x/sys 0.48.0, go-pkcs12 0.7.3), frontend npm packages (react/react-dom 19.3.0, react-router 8.4.0, axios 1.20.0, date-fns, jspdf-autotable, @noble/hashes & @noble/ciphers 2.4.0, lucide-react 1.x, @types/node 26, @testing-library/react & user-event, @vitejs/plugin-react 6, globals 17, autoprefixer, @tailwindcss/postcss, eslint-plugin-react-refresh, vite-plugin-node-polyfills), container base images (alpine 3.21 → 3.24 for init-certs, python 3.12-alpine → 3.14-alpine for the docs build stage), and GitHub Actions (actions/upload-artifact 4 → 7, actions/github-script 7 → 9, azure/aks-set-context 4 → 5, azure/login 2 → 3, codecov/codecov-action 4 → 7, github/codeql-action 3 → 4, docker/build-push-action 6 → 7, docker/setup-buildx-action 3 → 4, docker/login-action 3 → 4, zaproxy/action-baseline 0.14 → 0.15). Automated dependency PRs are now opened against the dev integration branch (Dependabot target-branch: dev for every ecosystem) instead of the default branch. Deferred (blocked upstream, kept as open tracked PRs): typescript 7 (blocked by typescript-eslint peer typescript <6.1.0) and eslint 10 / @eslint/js 10 (blocked by eslint-plugin-react peer, which does not yet declare eslint ^10).
  • Material UI Upgraded to v9 (Major, Frontend) — The admin console was migrated from Material UI 7 to @mui/material / @mui/icons-material 9.4.0 and @mui/x-date-pickers 9.14.0. All removed/renamed v9 APIs were migrated with the official MUI codemods plus targeted fixes: deprecated system style props (fontWeight, display, fontFamily, alignItems, fontStyle, spacing, …) moved into sx; TextField/Dialog/Checkbox/Autocomplete legacy slot props (InputProps, inputProps, InputLabelProps, SelectProps, PaperProps, TransitionProps) moved to the unified slotProps API; the removed *Outline icons were repointed to their *OutlineOutlined equivalents; and the theme's MuiButton containedPrimary style override was rewritten with the v9 variants API. This is a frontend-internal component-library upgrade with no change to application routes, behavior, or backend/API contracts; the full TypeScript type-check, ESLint, production build, and 165-test unit suite pass.
  • Frontend Build/Test Toolchain Requires Node ≥ 22.22 — The test toolchain was raised to vitest 5 and jsdom 30, whose bundled undici requires a Node runtime API (webidl.util.markAsUncloneable) available only on Node ≥ 22.22. CI and the frontend Docker image already run Node 24; .nvmrc files were added at the repository root and in frontend/ (pinned to Node 24) so local development selects a compatible runtime automatically.
  • Auth Cookies Are SameSite=Lax (OWASP ZAP finding) — The refresh_token and XSRF-TOKEN cookies were sent with SameSite=None whenever COOKIE_SECURE=true, which let browsers attach them to cross-site requests. Both are now always SameSite=Lax; COOKIE_SECURE still controls only the Secure flag. The web interface always calls the API on its own origin, and SSO sign-in (Entra, GitHub, GitLab, SAML) is unaffected. Existing sessions continue: the refresh cookie gets the new attribute on the next token refresh.
  • API Cache and Cross-Site Request Protection (OWASP ZAP findings) — API responses now default to Cache-Control: no-store, no-cache, must-revalidate and X-Content-Type-Options: nosniff, so login, refresh, SSO and status responses are never stored by browsers or proxies; CRL and ACME responses keep their own cache policy. A Fetch Metadata resource isolation policy rejects cross-site browser requests with 403 unless they are top-level navigations, the SAML assertion POST, or come from an origin listed in CORS_ALLOWED_ORIGINS. Non-browser clients (agents, gateways, SCEP/ACME/OCSP, CLI, Terraform) are unaffected. If a browser application on another domain calls the API, list its origin in CORS_ALLOWED_ORIGINS.
  • Server-Side Browser Sessions for the Web Interface — The backend can now keep browser sessions server-side (table ui_sessions, migrations 000208–000210) behind BFF-only endpoints /api/v1/ui/auth/* and the Authorization: Session scheme, which is accepted only together with the X-MazeVault-BFF header carrying BFF_SHARED_SECRET. Administrators can list and revoke a user's sessions with GET/DELETE /api/v1/rbac/users/{id}/sessions. The new web interface uses them (see "New Web Interface"). Sessions end after UI_SESSION_IDLE_TIMEOUT (30 minutes) of inactivity or UI_SESSION_ABSOLUTE_TIMEOUT (12 hours), a pending MFA step after UI_SESSION_MFA_TIMEOUT (5 minutes), and are revoked on password change (except the calling session), user deletion, role assignment or removal, MFA disable and SSO/LDAP role changes.
  • New settings — TRUSTED_PROXIES, UI_SESSION_IDLE_TIMEOUT, UI_SESSION_ABSOLUTE_TIMEOUT, UI_SESSION_MFA_TIMEOUT and the secret BFF_SHARED_SECRET (see Environment Variables). The installers and upgrade-mazevault.sh generate BFF_SHARED_SECRET; in Helm it is required (global.keyVault.secretKeys.bffSharedSecret, see Distribution Changes). upgrade-mazevault.sh installs the kit's docker-compose.yml (after the backup). Local developers re-run ./secrets.sh once.
  • BFF requests pass the CSRF cookie check — A state-changing request whose X-MazeVault-BFF header matches BFF_SHARED_SECRET (constant-time comparison) no longer needs the XSRF-TOKEN cookie pair: the frontend server (BFF) has already checked the browser's origin, and an anonymous call through it, such as claiming a shared secret, carries no cookies. An unset secret exempts nothing; other requests are checked as before.
  • Client IP is taken from X-Forwarded-For only from trusted proxies (behaviour change) — X-Forwarded-For sent by peers outside TRUSTED_PROXIES (default: loopback and private ranges in the backend and in Helm, the kit subnet MAZEVAULT_SUBNET in Docker Compose) is now ignored, which changes the client IP recorded in the audit log and used by rate limits when a proxy has a public address. List such proxies in TRUSTED_PROXIES. Conversely, where untrusted clients also have private addresses, set TRUSTED_PROXIES to the exact edge/ingress addresses or subnets, otherwise they can forge X-Forwarded-For.
  • MFA Session Tokens Are Single-Use — A token issued after the password step could be replayed on POST /api/v1/auth/mfa/verify until it expired; it is now consumed by the first successful verification.
  • Logout Revokes the Refresh Token — POST /api/v1/auth/logout now also clears the stored refresh token (a stolen one no longer survives logout) and writes a LOGOUT audit entry.
  • MFA Verification Is Rate-Limited and Capped — POST /api/v1/auth/mfa/verify is rate-limited, and an MFA session allows at most 5 code attempts; after that a new login is needed (audited as VERIFY_MFA_LOCKED).
  • SCIM Provisioning Changes Are Audited — Creating, updating and deleting a user and creating, patching and deleting a group through the SCIM routes (/api/v1/auth/scim/{providerID}) wrote no audit entry. They now record CREATE_SCIM_USER, UPDATE_SCIM_USER, DELETE_SCIM_USER, CREATE_SCIM_GROUP, PATCH_SCIM_GROUP and DELETE_SCIM_GROUP (entities SCIM_USER and SCIM_GROUP) with the client IP, attributed to the system user since the SCIM token identifies no person; the details name the provider and the resource, never the token.
  • Identity Provider Answers No Longer Contain the SCIM Token Hash — The identity provider routes (/api/v1/identity-providers) masked the secrets of a provider config but answered the stored scim_token_hash (the bcrypt hash of the provider's SCIM bearer token) in clear. It is now masked like client_secret, and an update that sends the mask back or leaves it empty keeps the stored hash.
  • SCIM Errors No Longer Contain Database Text — A failing SCIM operation answered 500 with the raw error of the service, so a non-UUID id on DELETE/PUT/PATCH under /api/v1/auth/scim/{providerID}/Users/{id} and /Groups/{id}, or a value the database rejected, returned PostgreSQL error text (invalid input syntax for type uuid ... SQLSTATE 22P02). An id that is not a UUID now answers 404 User not found / 404 Group not found, like an unknown one, and any other failure answers 500 Internal server error with the cause in the server log only.
  • LDAP Config Routes Answer No Stored Secret in Clear — GET and POST /api/v1/auth/sso/ldap/config masked only the bind password. If the LDAP provider config also held a client_secret, api_token, token or scim_token_hash, it was answered in clear. They now mask the same secrets as the identity provider routes (**** and the last four characters; the bind password stays the __MASKED__ sentinel), and saving the LDAP config keeps a stored secret that the request leaves out, empty or masked instead of dropping it.
  • An Inactive Identity Provider Takes No SCIM Calls — The SCIM routes (/api/v1/auth/scim/{providerID}) accepted creating, updating and deleting users and groups for an identity provider that was switched off (status inactive) as long as the SCIM token was right. Such a provider now answers every call, reads included, with 403 SCIM not enabled for this provider, the answer of a provider without SCIM, before the token is checked.
  • An Inactive Identity Provider Takes No SCIM Writes — The SCIM routes (/api/v1/auth/scim/{providerID}) accepted creating, updating and deleting users and groups for an identity provider that was switched off (status inactive) as long as the SCIM token was right. Such a provider now answers every write with 403 SCIM not enabled for this provider, the answer of a provider without SCIM, before the token is checked; reads still work.
  • v2 Secret Writes Are Checked in the Environment They Touch — The permission guard of POST /api/v2/secrets, PUT and DELETE /api/v2/secrets/{id} and POST /api/v2/secrets/migrate only looked at the environment the request resolved to, so a user whose role grants secret:write or secret:delete in dev only could create a secret in prod, or change, delete or migrate a prod secret. Each now needs the permission in the environment of the body or of the secret (and on its project, for a role scoped to a project) and answers 403 insufficient permissions for environment otherwise, before any change; the migration of a secret on a project the role does not cover is reported as secret not found. Requests for the environment they resolved to behave as before. Permission decisions are now cached per organization and project as well.
  • v1 Secret Writes Are Checked in the Environment They Touch — The permission guard of the v1 secret write routes only looked at the environment the request resolved to, so a user whose role grants secret:write, secret:delete, secret:archive or the archive lifecycle in dev only could create a secret in prod, or update, delete, roll back, copy, archive, restore, delete for good, sync or import into one. Create, import and the target of a copy now need the permission in the environment of the body; every route by secret id needs it in the secret's own environment (a copy also needs secret:read in the source's, a force sync of one environment in the synced one) and, for a role scoped to a project, on the secret's project. They answer 403 insufficient permissions for environment before any change. Requests for the environment they resolved to behave as before; a client that writes into another environment than it sends in the environment header or parameter now needs a role that covers that environment.
  • Secret Reads Are Checked in the Environment They Touch — The permission guard of the secret read routes only looked at the environment the request resolved to, so a user who may read dev only could read a prod secret by id: GET /api/v1/secrets/{id} returned its decrypted value, and its versions, sync status, offload status and sync log were readable too. These routes now need secret:read in the secret's own environment and, for a role scoped to a project, on its project, and answer 403 insufficient permissions for environment before anything is read or decrypted. GET /api/v1/projects/{id}/secrets without an environment filter lists only the environments the user may read, and the v2 reads and the comparison check the project as well. POST /api/v1/secrets/drift/detect without project_id (every project) needs the permission on every project, so a role on one project must pass its project_id. A failing permission lookup now refuses the request instead of letting it through.
  • Secret Listings Are Checked for the Project and the Requested Environment — GET /api/v2/projects/{id}/secrets and /secrets/matrix judged the environments a user may read without the project, so a user who may read dev everywhere and prod on project A only also got project B's prod secrets (their encrypted name, value and nonces). They now judge each environment for the project. GET /api/v1/projects/{id}/secrets?environment=X was judged in the environment the request resolved to, which on a server with configured environments is not X; it now needs secret:read in X itself and answers 403 insufficient permissions for environment otherwise.
  • The v2 Secret Matrix Is Empty When No Environment Is Readable — GET /api/v2/projects/{id}/secrets/matrix asked for every environment when the user could read none of the organization's environments, so a user who may read only an environment outside the organization's list (or anyone, in an organization without environments) got every secret row of the project. It now returns no rows in that case, like the list.
  • Environment- and Project-Scoped Roles Also Restrict Browser Users — A web interface session and a user's JWT carried the role shown on the user, and the permission check granted that role in every environment and project, on top of the user's scoped assignments. That role was the highest one the user held in any scope, so a user given Secret Manager for dev only could read and change prod secrets, and those of other projects, from the browser (API tokens were already restricted). A user now gets exactly the role assignments of the environment, project and organization a request touches, however they sign in; an agent's token keeps its role. An assignment scoped to a project or an organization counts only for a request in that project or organization; before, it also counted on every route without a project, so a role assigned for one project (for example admin) could assign itself any role everywhere. The project of a request is the one its path names or the one the secret (also an archived one being restored or permanently deleted), certificate, approval request or policy, project integration, sync rule or sync conflict, consistency group or consistency warning it names belongs to (an unknown id still answers 404). A project_id in the query or the JSON body counts only on the routes that act on that project (GET and POST /api/v1/secrets/, GET and POST /api/v2/secrets, POST /api/v2/secrets/compare, POST /api/v1/secrets/drift/detect, GET /api/v1/consistency-groups); elsewhere it is no scope, and one that differs from the route's project is refused with 403, whatever the body's Content-Type. The guard reads every write body before the handler, the way the handlers decode it (the first JSON value, keys in any case): a project_id/organization_id that is not a string counts as a project or organization where nobody holds a scoped role, a body it cannot read answers 400, and a body over 16 MiB 413; a caller without the route's permission anywhere is refused before its body is read. Sync rule, approval policy and configuration template updates no longer write any field the body names (gorm took column and Go field names, so a ProjectID key moved a sync rule or a policy to another project): only their mutable fields change, and a sync rule (also when created) takes only an integration of its own project. On PUT /api/v1/certificates/{id} and PUT /api/v1/rotation/templates/{id} the body's project_id is a move target: the permission must hold in the resource's project and in the target project. API token requests belong to the token owner's default organization, as browser sessions do; a request that names another organization (path, organization_id query or body field) needs the permission there as well. The routes that act on several projects (GET /api/v1/projects/, POST /api/v1/secrets/drift/detect without project_id, POST /api/v2/secrets/migrate) accept a role on any project and then act per project: the projects list returns only the projects the user may read. The secret naming check (POST /api/v1/organizations/{id}/secret-naming-policy/check) also accepts config:read in any project, so project users get the hint; creating the secret enforces the policy again. GET /api/v1/auth/me/permissions takes an optional project_id and then lists the permissions in that project, and under environments the permissions in that project for each environment of the organization; the permission lists now also count the assignments of the organization, and they add any_project_permissions, the permissions held in at least one project (which gate the Projects page and its sidebar entry). On a project page the web interface checks the permissions in that project, so a user whose role is on one project sees that project and its actions. The role shown on the user is now the highest role assigned without a scope (Viewer if there is none) and is a label only. Every path that creates a user (local, LDAP, Entra ID, GitHub, GitLab, OIDC, SAML, SPNEGO, SCIM and the start-up admin accounts) now also assigns its default role everywhere, and migration 000216 (idempotent) does that for existing users who have no role assignment at all, so they keep the role they held. A token role now only counts for a machine principal (an agent's or a service identity's JWT): the still valid JWT of a deleted user no longer carries its role.
  • Terraform and Go SDK Role Assignments Keep Their Scope — mazevault_user_role (and the Go SDK AssignRole) sent project_id and environment at the top level of POST /api/v1/rbac/users/{id}/roles, where the server read only scope, so the assignment was created without a scope: the role held in every project and environment. The SDK and the provider now send scope, and the server takes a top-level project_id/environment as the scope when scope is absent (a request where both are set and differ answers 400). mazevault_sync_rule.project_id now forces a replacement (a rule cannot move between projects).
  • Approval Requests Take Only the Requester's Fields — POST /api/v1/projects/{id}/approval-requests bound the whole request model, so a requester could create a request that was already approved, carried decisions, its own id, timestamps or expiry, or named a policy of another project. It now takes only request_type, resource_type, resource_id, action, details, reason, urgency and policy_id; the server sets the rest (a new request is pending, needs one approver and expires after 48 hours), and a policy of another project answers 400.
  • Certificate Routes Are Checked in the Certificate's Own Project and Environment — The permission guard of the certificate routes looked at the environment the request resolved to and, for a user whose role is assigned to one project, at the project the request named, not at the certificate itself. GET, PUT, POST …/archive, …/revoke, …/unrevoke and GET …/targets of /api/v1/certificates/{id} now judge the permission in the environment and project of the certificate, and answer 403 insufficient permissions for environment otherwise; an update that moves a certificate, or names an environment, is also judged in the project and environment it moves to. Restoring and permanently deleting a certificate (POST …/restore, DELETE …/permanent) now count a role scoped to the certificate's project, so a project administrator can run them for their own project (it was refused for every project-scoped role). A certificate without a project is reachable only for a user whose permission is not limited to some projects.
  • The Certificate List Shows Only What the User May Read — GET /api/v1/certificates/ (and its enriched view) is now an "any project" route: a user whose role is assigned to one project (or limited to some environments) passes the permission guard and gets the certificates they may read in the certificate's own project and environment, instead of 403 for a role assigned to one project. A role limited to some environments but not to a project already passed the guard (in the environment of the request) and got the certificates of every environment, prod included; it now gets those of the environments it may read. project_id only filters the list and never widens it. A certificate without a project is listed only for a user whose permission is not limited to some projects; a user who may read none gets an empty list.
  • Certificate Export Is Checked in the Certificate's Own Environment and Project — GET /api/v1/certificates/{id}/export only needed certificate:read in the environment the request resolved to, so a user whose role is limited to one environment could download a certificate (with its private key, when requested) of another environment. It now judges the permission in the environment and project of the certificate and answers 403 insufficient permissions for environment otherwise; a certificate without a project is exported only for a user whose permission is not limited to some projects.
  • Project Certificate and Secret Manager Routes Are Checked in the Row's Own Environment — GET /api/v1/projects/{id}/certificates/{certId} and the certificate list of a project (GET …/certificates) only needed certificate:read in the environment the request resolved to, so a user whose role is limited to dev could read a prod certificate by id and see it in the list. The by-id route now needs the permission in the certificate's environment, and the list keeps the certificates of the environments the user may read. GET …/integrations/{integrationId}/secrets is checked in the environment the integration is bound to, and GET …/integrations/secret-managers (which lists the integrations of every project of the organization) keeps only those the user may read in their own project and environment: a user whose role is assigned to one project no longer sees the names and providers of the other projects' secret managers there.
  • A Template Assignment Takes Only a CA Account of the Project's Organization — POST /api/v1/projects/{id}/templates/{template_id} stored the override_ca_account_id of the body without checking it, so a project could be set to issue from a CA account of another organization (or one that does not exist). The account must belong to the organization of the project; otherwise the route answers 400 and assigns nothing.
  • The Certificate Revocation Status Is Checked in the Certificate's Own Environment and Project — GET /api/v1/certificates/{id}/status had no permission check, so any signed-in user (also a role with no certificate permission, or one assigned to another project or environment) could read the status, serial number and revocation date of any certificate id. It now needs certificate:read in the environment and project of the certificate and answers 403 insufficient permissions for environment otherwise.
  • Project HSM and External CA Routes Removed — POST /api/v1/projects/{id}/ca/{caId}/hsm and POST /api/v1/projects/{id}/ca/{caId}/external/configure used the project id in the path as a CA id, so a user assigned ca:admin for a whole organization (not limited to a project) could set the HSM configuration, or store an external CA configuration, of any CA by putting its id there, including a CA of another organization. The routes are removed; the console and the clients use POST /api/v1/ca/{id}/hsm and POST /api/v1/ca/{id}/external/configure. Those routes check that the CA belongs to the caller's organization (see "Certificate Authority Routes Stay in the Caller's Organization").
  • A Role Assigned to One Organization No Longer Reaches the Projects of Another — The permission check of a project route (/api/v1/projects/{id}/..., and routes that name a certificate or another resource that belongs to a project; the /api/v1/ca/{id} routes name a CA and are covered by "Certificate Authority Routes Stay in the Caller's Organization") judged the caller's own organization (the default one for a console session or an API token) and never the organization of the project, so an administrator assigned to one organization could read and change the CAs, certificates and other resources of a project of another organization by its id. The permission must now also hold in the project's organization; roles that are not limited to an organization, and requests inside one organization, are unchanged.
  • Renewal Queue Routes Are Judged in the Project and Environment of the Certificate — The renewal queue (/api/v1/renewal-queue/…) and the renew and renewal-history routes of a certificate checked the permission without the project and environment of the certificate: a role scoped to one project got 403 for every queue item, and a role limited to dev could read, cancel, resume and renew the items and certificates of prod. A queue item now belongs to the project of its certificate: approve, cancel, resume and the DCV routes need their permission in that project and in the certificate's environment (and in the organization of the project), POST /api/v1/certificates/{id}/renew and GET …/renewal-history need it in the certificate's own project and environment, and GET /api/v1/renewal-queue/ keeps the items whose certificate the user may read. A certificate without a project is judged strictly (only a role that is not limited to some projects reaches it); an unknown queue item is still answered by the route.
  • The Organization Check Also Covers Projects That Were Deleted — The check that a role assigned to one organization does not reach the projects of another read the project's organization with the normal query, which skips a project that was deleted (softly, as DELETE /api/v1/projects/{id} does), so the CAs, certificates and other data a deleted project of another organization still holds were reachable by id for an administrator of the default organization. The organization is now read including deleted projects, and a request is refused when it cannot be read; a refused decision is cached like an allowed one.
  • The Project List Shows Only the Projects of the Caller's Organization — GET /api/v1/projects/ returned every project of every organization to a user whose role is assigned to an organization, because the role counted as held "everywhere" and the list was not filtered. A project of another organization than the request's is now checked on its own, so such a user sees the projects of their organization only; a role that is not limited to an organization still lists all.
  • Certificate Authority Routes Stay in the Caller's Organization — GET, PUT /api/v1/ca/{id}, POST /api/v1/ca/{id}/hsm, …/external/configure, …/external/test, …/external/templates/sync and …/crl/regenerate did not check which organization the CA belongs to, so an administrator assigned to one organization could read another organization's CA, rewrite its HSM settings, OCSP and CRL URLs, store an external CA configuration for it and regenerate its CRL by its id, and GET /api/v1/ca/ listed the CAs of every organization. A CA belongs to the organization of its project or of the CA account it is linked to. These routes now need the permission in that organization (an assignment that is not limited to an organization still reaches every CA) and answer 403 Access denied for a CA of another organization, for a CA that belongs to no organization (created by POST /api/v1/ca/initialize: only an assignment that is not limited to an organization reaches it) and for an id that names no CA, the same answer in all three cases so that ids cannot be probed. The list returns only the CAs the caller may read. Check the role assignments of API tokens that use /api/v1/ca/: a token with ca:admin limited to one organization no longer reaches CAs that belong to no organization.
  • Certificate Templates Stay in the Caller's Organization — GET, PUT and DELETE /api/v1/certificate-templates/{id} did not check which organization the template belongs to, so an administrator assigned to one organization could read, change or delete another organization's template by its id (an update also changes the renewal settings of the certificates that follow the template). The permission is now judged in the template's organization; another organization's template and an unknown id answer 403. The template reads (GET /api/v1/certificate-templates/ and /{id}) are also open to a role assigned to a single project, for the templates of its own organization, because the project Templates tab and the certificate request dialog need them: such a role got 403 on the list before. Another organization's templates need an assignment of that organization or one that is not limited to an organization.
  • Project Templates Stay in Their Organization and No Longer Rewrite Other Certificate Templates — PUT /api/v1/organizations/templates/{templateId} took the linked certificate template from protocol_config.certificate_template_id of the request and overwrote that template, so a holder of template:write could rewrite the certificate template of another organization or a shared system template (name, validity, key usage, approval, CA account; the renewal settings of every certificate following it changed with it), and both routes changed or deleted a project template of any organization. The link is now set by the server only: a certificate_template_id in the request is ignored, and a stored link to a template of another organization or a system template is not followed (the project template is linked to a template of its own organization instead). The update and DELETE judge the permission in the template's organization and answer 403 for another organization's template (and an unknown id, unless the role is not limited to an organization, which gets 404; both answered 500 before). Creating and updating a certificate-category project template now checks the CA account and product like the certificate template routes (400 for those of another organization, 409 for a code taken by a system template).
  • A Template Without a CA Issues Only From Its Organization's Internal CA — When a certificate template named no CA account and no CA, issuance signed with whichever active internal CA the database returned first, which could be the internal CA of another organization. Such a template now issues only from the internal CA of its own organization (the template's, which is the project's) (the certificate records that CA account); without one the request is refused with 400 ("template has no CA assigned and the organization has no internal CA"). System templates such as agent-mtls keep using the active internal CA.
  • Discovered Certificates Need an Assignment Not Limited to an Organization, and Archived Certificates Are Listed per Organization and Environment — Discovered certificates have no organization (one record per fingerprint, whichever organization's agent or CA account reported it), yet a role assigned to one organization listed, read, ignored, archived, assigned and adopted every organization's discovered certificates. The /api/v1/discovered-certificates routes now answer 403 to a role limited to an organization or a project; a role without those limits (an environment limit is kept) works as before, so assign one to users who triage discovery. GET /api/v1/archived/certificates and the certificates of GET /api/v1/archived/summary listed the archived certificates of every organization and environment; they now keep those the caller may read (archive:read) in the certificate's own project, organization and environment, and the summary lists archived discovered certificates only to a role not limited to an organization or a project.
  • Certificate Export Answers 404 for an Unknown Certificate and No Longer Sends Error Text — GET /api/v1/certificates/{id}/export answered 500 "record not found" for an unknown certificate and sent the internal error text of any other failure (for example failed to parse certificate: x509: …). An unknown certificate now answers 404 "Certificate not found"; a format the caller can change (DER with include_chain, JKS without a private key, an unsupported format) answers 400 with its message (it was 500); any other failure answers the fixed Internal server error and the cause is written to the server log only.
  • Agent mTLS Verifies the Client Certificate Chain — POST /api/v1/agents/me/refresh-token and the legacy agent update download accepted any client certificate (on the TLS connection or in the X-Client-Cert / ssl-client-cert header of a trusted proxy) whose serial number matched an agent certificate, without verifying who signed it, so a self-signed certificate copying that serial obtained a fresh agent token. The certificate must now be the agent certificate MazeVault issued for that serial (same SHA-256 thumbprint), not revoked, valid now, allowed for client authentication, and chain to the CA that issued it (the CA recorded on the certificate; for certificates recorded before the CA was, one of the internal CAs). Any other certificate answers 401 CLIENT_CERT_INVALID with no detail (an expired one answered CERT_EXPIRED before); the reason is logged. Deployed agents keep working without re-enrolment: their certificates were issued by the internal CA and are recorded. An agent whose issuing CA has been deleted must be enrolled again.
  • A Certificate Deployment Sync Is Judged in the Certificate's Own Environment, for a Target of That Certificate — POST /api/v1/certificates/{id}/targets/{targetId}/sync judged rotation:execute only in the environment the client named and ignored the certificate in the path, so a user limited to dev deployed a prod certificate to its target by sending ?environment=dev, and any target could be synced under any certificate id. The target must now belong to the certificate in the path (otherwise 404), and the sync is judged in the environment and project of that certificate, as the canonical POST /api/v1/rotation/resources/certificate_deployment/{id}/trigger is; both run the same code. A failed sync answers the fixed "Internal server error" body instead of the step's error text and is audited with outcome=failed.
  • The Rotation Routes of a Certificate Are Judged in the Certificate's Own Environment — Eligibility, rotation config, history, executions, the renewal and deployment preflights, the deployment targets and the format preview under /api/v1/certificates/{id}/… judged their permission in the environment the client named, so a user limited to dev read and changed the rotation config and the deployment targets of a prod certificate by sending ?environment=dev. Each route now judges its permission in the environment and project of the certificate in the path, and an unknown certificate answers 404 "Certificate not found" (the config, preflight and target create answered 500 with the database error, the history an empty list).
  • A Certificate's Targets and Executions Are Reached Only Under That Certificate, and a Target Delivering to Another Project Is Judged There Too — GET, PUT, DELETE /api/v1/certificates/{id}/targets/{targetId} and GET …/status ignored the certificate in the path, so a user who could act on one certificate read, changed or deleted the deployment target of any other certificate, and GET /api/v1/certificates/{id}/rotation/executions/{execId} returned any execution. The target or execution must now belong to the certificate in the path (otherwise 404). A target receives the certificate and its private key, so creating or changing one now also needs secret:write where it delivers, unless that is the certificate's own project and environment: for an integration (integration_id), in the integration's project and environment; for a secret-manager target, in the project and environment the delivery writes to (the integration's configured target_project_id and environment, else the target's target_project_id and target_environment, else production), and for a target_secret_id also in that secret's own project and environment.
  • A Secret Rotation Target Delivers Only to an Agent of the Secret's Project — A secret deployment target (POST, PUT /api/v1/secrets/{id}/rotation/targets…) whose config.agent_id named any agent, including one of another organization, was accepted with secret:rotate on the secret, and every rotation then sent the new secret value to that agent. The agent must now be bound to the secret's own project (an active project agent integration), otherwise create and update answer 404. Targets created earlier are checked again when the rotation runs: the agent sync step of a target whose agent is not bound to the secret's project fails without sending anything and logs the refusal (never the value).
  • A Secret Target Writing Into Another Environment of the Project Is Judged There — A deployment target of a secret that named an integration of the secret's own project in another environment (binding_ref or config.integration_id, for example the prod integration for a dev secret) needed only secret:rotate on the secret, so a rotator limited to dev made every rotation write into prod. Such a target now also needs secret:write in the integration's environment (403 otherwise), as a target naming an integration of another project already did.
  • Keytab History, Compliance Check and Discovered Import Stay in the Caller's Organization — GET /api/v1/keytabs/{id}/history, POST /api/v1/keytabs/{id}/check-compliance and POST /api/v1/keytabs/discovered/{id}/import did not compare the keytab with the caller's organization: any holder of keytab:read or keytab:write could read the version history of another organization's keytab, re-run its compliance check or turn its discovered keytab into a keytab of that organization. Like get, update, download and delete they now answer 404 for a keytab or discovered keytab of another organization and for an unknown id (the history of an unknown keytab was an empty list). An API token names its organization with organization_id on these routes, as on the others.
  • Keytab Download Answers Cache-Control: no-store — GET /api/v1/keytabs/{id}/download is the deliberate, permission-gated reveal of a stored keytab (and stays audited, never with the value). Its answer now carries Cache-Control: no-store alone, so no browser or proxy keeps the keytab; the OpenAPI document and the generated client describe the header.
  • Audit Log, Compliance Report and Terraform Export Failures No Longer Send Internal Error Text — POST /api/v1/audit-logs/search, GET /api/v1/audit-logs/, GET /api/v1/projects/{id}/audit-logs, GET /api/v1/compliance/{type} and GET /api/v1/terraform/export/project|organization/{id} answered a failed database or service call with its error text in the HTTP 500 body. They now answer the fixed Internal server error; the cause is written to the server log only.
  • Terraform Organization Export Checks the Organization in the URL — GET /api/v1/terraform/export/organization/{id} judged the org:read permission for the organization of the request only, so a user limited to one organization could export any other organization by id. The id is now checked against the caller's organization assignments (403 for another organization).
  • Organization Integration Test No Longer Decrypts Request Values — POST /api/v1/organizations/{id}/integrations/test decrypted any enc: value of the request with the server master key before sending the test notification, so a holder of integration:read could send a stored ciphertext (for example a Jira api_token with its own base_url) and receive the plaintext at a host of their choice. The route no longer decrypts request input: a configuration containing an enc: value answers 400 invalid integration config and nothing is sent.
  • Organization Integrations No Longer Return Stored Secrets — GET /api/v1/organizations/{id}/integrations returned the decrypted credentials of every integration (Jira API token, Teams/Slack/webhook URLs, the Office 365 client secret) and the plaintext credentials of log streams and archives (Splunk HEC token, Elasticsearch API key and password, S3 secret access key, Azure Blob account key, SAS token and connection string, file share password); POST /api/v1/organizations/{id}/integrations and PUT /api/v1/organizations/integrations/{integrationId} returned the encrypted values. These fields now answer ******** when set and "" when empty. An update that sends ******** back or leaves a secret field out keeps the stored value; "" clears an optional secret and keeps a required one (Jira api_token, Teams/Slack webhook_url, webhook url, Splunk hec_token, S3 secret_access_key).
  • Organization Integration Updates and Deletes Check the Integration's Organization — PUT and DELETE /api/v1/organizations/integrations/{integrationId} judged integration:write for the organization of the request only, so a user limited to one organization could change or delete another organization's notification channels, log streams and archives by id. The integration's own organization is now checked against the caller's assignments (403 for another organization).
  • Integration URLs Refuse the IPv6 Loopback Address — The SSRF check of organization integration URLs (Jira, Teams, Slack, webhook) compared the host with [::1], which a parsed URL never yields, so http://[::1]/… passed and a test notification or report could be sent to a service on the server itself. ::1 is now refused like 127.* and localhost.
  • Weekly Report Failures No Longer Send Internal Error Text — POST /api/v1/reports/trigger and GET /api/v1/reports/preview answered a failed delivery or database call with its error text in the details of the HTTP 500 body, including the delivery hosts (for example a decrypted webhook URL) and recipients. They now answer Failed to trigger report / Failed to generate preview without details; the cause is written to the server log only.
  • Integration URLs Refuse Local IP Addresses in Every Encoding — The SSRF check of organization integration URLs (Jira, Teams, Slack, webhook) compared IP hosts as text, so other spellings of a local address passed ([0::1], [0:0:0:0:0:0:0:1], [::ffff:127.0.0.1], [::], [fe80::1%eth0]). The host is now parsed as an IP (brackets and zone removed), and loopback (127.0.0.0/8, ::1), unspecified (0.0.0.0, ::), link-local (169.254.0.0/16 including the cloud metadata address 169.254.169.254, fe80::/10) and multicast addresses are refused in any encoding, as are localhost and an empty host. Private ranges (172.16.0.0/12, fd00::/8) and host names that resolve to internal addresses are not refused by this check.
  • Integration URLs Refuse localhost. and *.localhost — The SSRF check of organization integration URLs accepted localhost. (with a trailing dot) and names under .localhost (for example api.localhost), which resolve to the server itself. Both are now refused like localhost.
  • Log Stream and Archive Credentials Are Encrypted at Rest — The Splunk HEC token, Elasticsearch API key and password, S3 secret access key, Azure Blob account key, SAS token and connection string and the file share password of organization integrations were stored in plaintext in the database (only the notification credentials were encrypted). They are now encrypted with the server master key like the others. Existing rows need no migration: they are still read as they are and are encrypted on their next update.
  • A Kept Integration Secret Cannot Move to Another Endpoint — PUT /api/v1/organizations/integrations/{integrationId} kept a stored secret sent back as ******** (or left out) even when the same request changed where it is sent, so a holder of integration:write could point a Jira base_url, Splunk endpoint, Elasticsearch endpoints/cloud_id, S3 bucket/region/endpoint, Azure Blob account_name/container_name, file share base_path or Office 365 tenant_id/client_id at their own host and receive the stored credential. Such an update now answers 400 invalid integration config and changes nothing; enter the secret again when changing the endpoint.
  • Testing a Saved Organization Integration Works With Masked Secrets — Since secrets are answered as ********, POST /api/v1/organizations/{id}/integrations/test and .../test-archive take an optional integration_id of the saved integration (same type): secret fields sent as ******** or left out are taken from it, as long as its endpoint fields (URL, host, account, bucket, path) are unchanged; a changed endpoint needs the secrets entered again (400 invalid integration config). The saved integration is judged in its own organization (403 for another organization, 404 when unknown). A failed test of a saved integration answers integration test failed (archive test: Connection failed) instead of the provider error, which could contain a stored webhook URL; the cause is logged.
  • Failed Tests of a Saved Integration No Longer Log Its Secret URL — The server log line of a failed …/integrations/test or …/test-archive of a saved integration (integration_id) carried the provider error, which quotes the stored Slack, Teams or webhook URL; it now omits the URL and masks every stored secret value.
  • Project Integration Reads No Longer Return Stored Secrets — GET /api/v1/projects/integrations/{integrationId} answered the decrypted configuration: Vault tokens, client secrets (including the Entra ID single sign-on client secret copied into on-behalf-of integrations), passwords and keys, and the value of a linked secret as password, to any holder of integration:read, who could so read a secret without secret:read. Every secret field that is set now answers ********, the linked secret's value included. PUT of the same integration keeps the stored value for a secret field sent as ******** or left out, but only while the endpoint fields (URLs, hosts, ports, tenant, region, TLS settings) are unchanged; changing an endpoint while keeping a secret answers 400 invalid integration config and changes nothing. A saved form no longer copies the linked secret's value into the configuration.
  • Project Integration Routes Check the Integration's Environment — The routes by integration id (GET, PUT, DELETE /api/v1/projects/integrations/{integrationId}, its deletion-impact, test, sync and sessions routes) judged integration:read / integration:write in the environment of the request, so a user allowed in dev only could read, change, test, sync or delete a prod integration. They are now judged in the integration's own environment and project (403 otherwise), and an unknown id answers 404 integration not found instead of 500. GET /api/v1/projects/{id}/integrations/grouped lists only the environments the user may read, GET /api/v1/projects/{id}/environments/{env}/integrations is judged in {env}, and POST /api/v1/projects/{id}/integrations/test-connection in the environment of its body.
  • Testing a Saved Project Integration Works With Masked Secrets — Since the secrets of a project integration are answered as ********, POST /api/v1/projects/{id}/integrations/test-connection takes an optional integration_id of the saved integration (same project and provider): secret fields sent as ******** or left out are taken from it, as long as its endpoint fields are unchanged; a changed endpoint needs the secrets entered again (400 invalid integration config). The saved integration is judged with integration:write in its own environment and project (403, 404 when unknown). The failure message of such a test masks the stored secret values.
  • Project Integration Errors No Longer Send Internal Error Text — The database session routes (GET …/sessions, POST …/sessions/terminate, POST …/sessions/drain of /api/v1/projects/integrations/{integrationId}), the connection test, the manual sync and the integration lists answered a failed service or database call with its error text in the HTTP 500 body. They now answer Internal server error; the cause is written to the server log only. An invalid integration id of GET, PUT, DELETE /api/v1/projects/integrations/{integrationId} and its deletion-impact answers 400 Invalid Integration UUID, and an update body of the wrong shape 400 with the binding error, instead of 500.
  • Database Integration Secret References Need Secret Read Permission — A database integration can name its admin or wallet password by reference (_password_source mazevault or keyvault); the server resolves it and sends the value to the database host of the configuration. A holder of integration:write without secret:read could so have any secret of the project sent to a host of their choice through POST /api/v1/projects/{id}/integrations/test-connection, store such a reference with the create or update, or resolve one with the test, sync and sessions routes; a secret manager source integration of any project or organization was accepted. A request now resolves or stores a reference only when the caller holds secret:read in the referenced secret's project and environment, and, for a source integration, integration:read and secret:read in that integration's own project and environment (403 otherwise); a reference in a connection test without an environment answers 400. Scheduled syncs of saved integrations resolve as before. A database integration create that failed (for example a duplicate name) no longer ends in a server panic.
  • A Project Integration Update Cannot Move the Admin Password or a Linked Secret to Another Host — The rule that a kept secret cannot follow a changed endpoint covered only the secrets inside the configuration. PUT /api/v1/projects/integrations/{integrationId} could move a database integration's host or port while its stored admin password stayed, or move the endpoint of an integration whose credential comes from a linked secret, and the next test or session call sent that password or secret value to the new host. Such an update now answers 400 invalid integration config and changes nothing: a database integration's endpoint moves only with its admin_password entered again, and an integration with a linked secret keeps its endpoint.
  • The Stored Project Integration Test Masks Stored Secrets in Its Failure Text — POST /api/v1/projects/integrations/{integrationId}/test answered a failed test with the provider's error text and kept it as the integration's last_sync_error; a provider that quotes the credential it received (a token, a password in a connection string) exposed the stored secret to anyone with integration:read. Every stored secret value in that text is now replaced by ********.
  • A Kept Project Integration Secret Cannot Be Routed Through Another Agent — PUT /api/v1/projects/integrations/{integrationId} could switch use_agent_proxy or change agent_id while the stored secrets stayed, sending them through an agent the caller chose. Agent routing now counts as an endpoint: changing it while a stored secret is kept answers 400 invalid integration config; enter the secrets again to change it.
  • More Project Integration Fields Count as Endpoints — The AWS region (aws_region, region) and the Oracle network encryption settings (use_nne, encryption_level, encryption_types, checksum_level, checksum_types) now count as endpoint fields: a project integration update that changes them while keeping a stored secret answers 400 invalid integration config.
  • Integration Group Routes No Longer Return Database Error Text — A failing GET or POST /api/v1/projects/{id}/integration-groups and GET, PUT or DELETE /api/v1/integration-groups/{id} answered HTTP 500 with the raw database error (table, constraint and SQL state). They now answer {"error": "Internal server error"}; the cause is only in the server log.
  • Integration Groups Link Only Integrations of Their Own Project — The mappings of an integration group (POST /api/v1/projects/{id}/integration-groups, PUT /api/v1/integration-groups/{id}) were stored without checking the integration ids, so a group could name an integration of another project or organization that its owner may not read. Every mapping must now name an integration of the group's project; a malformed, unknown or foreign id answers 400 mappings must name integrations of the project and nothing is changed. Reading, updating and deleting a group by id, and listing or creating groups of a project, are judged in the group's own project and organization.
  • Database Integration Update Judges Secret References in the Configuration — PUT /api/v1/projects/integrations/{integrationId} of a database integration stored a reference in the configuration (the Oracle wallet password, _wallet_password_source mazevault or keyvault) without checking that the caller may read the referenced secret, while the create did. A holder of integration:write without secret:read could so have the secret sent to the database host on the next connection. The update now judges the references of the written configuration like the create (403 without secret:read in the referenced secret's project and environment).
  • The Managed Identity Check Covers Only the Organization of the Request — POST /api/v1/admin/azure/mi-permissions-check tested the Azure integrations of every organization and returned their names, Key Vault URLs and environments to a holder of integration:write in any one of them. It now lists and tests only the integrations of the projects of the request's organization.
  • An Incident Is Read Only Within Its Own Organization — GET /api/v1/incidents/{id} returned any incident by its id, including one of another organization. The incident is now judged where it lives: a role limited to an organization gets 403 for another organization's incident, and the environment and project of the incident are checked as on the resolve route. An unknown id still answers 404; a database failure now answers 500 {"error": "Internal server error"} instead of 404.
  • Dismissing an Actionable Rotation Row Stays Within Its Organization — POST /api/v1/dashboard/rotation/actionable/{id}/dismiss wrote the dismissal into the rotation resource of any organization by its id. A resource of a project is now judged where it lives: a role limited to an organization gets 403 for a resource of another organization's project and nothing is written, and dashboard:write must also hold in the resource's environment and project.
  • Certificate Reports and the Incident List No Longer Return Database Error Text — A failing GET /api/v1/dashboard/certificates/reports, POST /api/v1/dashboard/certificates/refresh or GET /api/v1/incidents/ answered HTTP 500 with the raw database error. They now answer {"error": "Internal server error"}; the cause is only in the server log.
  • Platform Rotation Rows on the Overview Need an Unrestricted Role — GET /api/v1/dashboard/rotation/actionable listed rotation rows of no project (system keys, service identities, SSH key pairs of no project) to roles limited to an organization, and POST /api/v1/dashboard/rotation/actionable/{id}/dismiss let them dismiss those rows. As on the Rotation Problems routes, such rows are now listed only for a role limited to neither a project nor an organization, and a dismiss by any other role answers 403 and writes nothing.
  • Project Integration Create and Agent Bindings Check the Environment — POST /api/v1/projects/{id}/integrations and POST /api/v1/projects/{id}/integrations/agents judged integration:write in the environment of the request, not in the environment of the body, so a user allowed in dev only could create an integration or bind an agent in prod. GET /api/v1/projects/{id}/integrations/agents listed the bindings of every environment, and DELETE /api/v1/projects/{id}/integrations/agents/{agentId} removed a binding of any environment. The create and the bind are now judged in the body's environment, the list keeps the bindings of the environments where the user holds integration:read (and the bindings of no environment), and the unbind is judged in the binding's own environment (403 otherwise, nothing changed). Unbinding an agent the project has not bound answers 404 instead of 204.
  • The Entra Scope of an Integration Is Judged Where the Integration Lives — GET and PUT /api/v1/entra/{integrationId}/scope read and replaced the Entra scope of any integration by its id, including one of another environment, project or organization, judging entra:read / entra:write only in the environment of the request. They are now judged in the integration's own environment and project (403 otherwise, nothing changed), and an unknown id answers 404 integration not found instead of 400 with the database text. POST /api/v1/entra/projects/{id}/scope-preview is judged in the environment of its body.
  • An Agent Is Bound Only to Projects of Its Own Organization — POST /api/v1/projects/{id}/integrations/agents bound an agent of any organization to the project, and a bound agent with allow_deployment receives the project's deployments. An agent registered in another organization than the project's is now refused with 404 Agent not found, as an unknown agent, and nothing is written. Agents registered before agents carried an organization stay bindable.
  • Entra Scope Routes No Longer Return Internal Error Text — GET and PUT /api/v1/entra/{integrationId}/scope, POST /api/v1/entra/projects/{id}/scope-preview and the Entra scope step of POST /api/v1/projects/{id}/integrations answered every failure 400 with its error text, a database, decryption or Microsoft Graph failure included. Input the caller can correct (an integration of another provider, an incomplete or unsupported binding, incomplete preview credentials) still answers 400 with its message; any other failure now answers 500 {"error": "Internal server error"} and the cause is only in the server log.
  • A Database Wallet Password Must Be Entered Again When Its Source Changes — PUT /api/v1/projects/integrations/{integrationId} of a database integration could change _wallet_password_source (static, mazevault, keyvault) or _wallet_password_source_integration_id while wallet_password was left out or sent as ********; the stored value was then kept under the new source, so a key vault path became the static wallet password, or the real password was resolved as a secret key and quoted in the sync error. Such an update now answers 400 wallet_password must be re-entered when its source changes and changes nothing; an update that sends the new value is saved, and an explicit empty wallet_password clears the stored one (for example when an Oracle integration moves from TCPS to network encryption and no longer uses a wallet). The admin password already required this.
  • Integration Sync Failures Mask Secret Values — POST /api/v1/projects/integrations/{integrationId}/sync, and the sync and connection-test runs that record a project integration's sync status, stored and answered the failure text unchanged in last_sync_error and the sync message. A failed secret reference quotes the value it tried to resolve (for example MazeVault secret "…" not found), so a password stored under the wrong source could appear there and in the server log. Every secret value of the integration's configuration (stored or resolved) in that text is now replaced by ********, also where the text quotes it with " and \ escaped; this escaped form is now also masked in the failure text of the project integration connection tests. The same masking applies to every other writer of a project integration's last_sync_error: the sync rules, the Entra ID sync and the rotation steps that record an integration's failure.
  • Incident Contacts Need Organization Permissions — GET and PUT /api/v1/organization/incident-contacts needed only a sign-in, so any user or API token could read the organization's incident contacts (names, e-mail addresses, phone numbers) and replace them, redirecting the incident notifications. Reading now needs org:read and replacing needs org:admin; without them the routes answer 403 and nothing changes. The Incident Response tab of the organization settings is shown only with org:read, and its editing only with org:admin.
  • Log Stream and Archive Endpoints Refuse Local Addresses — The SSRF check of organization integrations covered only notification URLs (Jira, Teams, Slack, webhook). The Splunk endpoint, every Elasticsearch endpoints entry, the syslog host and the Azure Blob host (from account_name, or the connection string's BlobEndpoint or AccountName/EndpointSuffix) were not checked, so an audit stream with its HEC token or credentials could be pointed at a service on the server itself or at the cloud metadata address. POST /api/v1/organizations/{id}/integrations, PUT /api/v1/organizations/integrations/{integrationId} and POST /api/v1/organizations/{id}/integrations/test-archive now refuse loopback (127.0.0.0/8, ::1), unspecified, link-local (169.254.0.0/16 including 169.254.169.254, fe80::/10) and multicast addresses in any encoding, localhost and names under .localhost with 400 and the check's message; nothing is saved or dialed. Private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) stay allowed for these endpoints.

Notes

  • Upgrade Impact (Scoped Roles for Browser Users) — Environment- and project-scoped roles now also restrict browser users: check your role assignments (Access Control) before upgrading. A user whose access in the web interface came from a role assigned for one environment or project keeps it only there; assign the role without a scope where it should hold everywhere. A user whose roles are all assigned for projects no longer reaches the routes that name no project (user and role management, organization settings, creating projects): assign the role needed there without a project, or for the organization. Until their web interface parts are rebuilt, these routes answer 403 to project-scoped users and project-scoped API tokens (SDK, Terraform, CLI) even with a project_id: SSH keys, keytabs, managed tokens, configuration management, rotation templates, certificate template writes, CA initialization, group mappings, dashboards, reports, incidents and Azure discovery; the discovered-certificate routes answer 403 to every role limited to a project or an organization (see "Discovered Certificates Need an Assignment Not Limited to an Organization"). The certificate list, analytics, import and CSR routes accept a project-scoped role in its own project and environment, and certificate template reads accept a role assigned to any project of the template's organization. Moving a certificate to another project (PUT /api/v1/certificates/{id}) needs the permission in both projects. Role assignments created by Terraform or the Go SDK with project_id or environment before this release are unscoped: re-apply the Terraform ones (terraform apply -replace=…), and delete and assign again the ones made through the Go SDK. GET /api/v1/rbac/users/{id}/roles lists a user's assignments; one without project_id and environment holds everywhere. Every guarded write body is now capped at 16 MiB (413 above), so a keytab is at most 11 MB: MAZEVAULT_KEYTAB_MAX_SIZE_MB above 11 is treated as 11 (a warning is logged). A user loses the shown role if all its assignments of that role have expired, or if the shown role was higher than any role it is assigned (it was never re-synced). Migration 000216 (idempotent, no down step: removing the assignments would remove access) assigns, without a scope: the shown role to users without any assignment; Viewer to every existing user of an identity provider (LDAP, Entra ID, SAML/SCIM, GitHub, GitLab, OIDC), as such users are now created with it, so a provisioned user whose shown role lacked Viewer's permissions (for example Auditor or Finance; by default dashboard:read, system_output:read, entra:read) gains them; and, only if no user then holds admin without a scope, admin to every user whose shown role is admin. The server logs a warning at start-up while no user holds admin without a scope. When nobody holds admin without a scope and some users whose shown role is admin hold an expired admin assignment without a scope, the lockout guard makes those assignments live again and the other users whose shown role is admin get nothing; only when no such expired assignment exists does every user whose shown role is admin get a new one. The migration also re-derives the shown role of every user who has assignments from their unscoped ones (users with only scoped roles now show Viewer). These also narrow silently: an API token user whose assignment is scoped to an organization other than the default one loses it on every request; a project-scoped user does not pass the checks that name no project (project export, SSH key administration, rotation backup reads, Key Vault integration reads); and permission decisions cached before the upgrade (Redis) keep applying for up to CACHE_PERMISSION_TTL_SECONDS (default 5 minutes) afterwards, so flush that cache (or wait that long) to apply the new rules at once. The same window applies after a resource moves to another project (a certificate or a rotation template): decisions cached for it before the move hold until they expire. A restart no longer widens an admin assignment of admin@mazevault.com that was scoped to an environment or a project.
  • Upgrade Impact (Effective Chain, Discovery Body, Agent Deployment Contract) — Version 1.0.61 adds one idempotent, additive migration, 000199_discovered_certificate_body (certificate_pem, chain_pem TEXT NULL on discovered_certificates; the database health check validates both). No column is added to certificates, no backfill runs, no existing chain is rewritten. Certificates that already carry a stored chain export, display, rotate and deploy byte-for-byte as before (covered by a regression test); only certificates with an empty chain change behavior, and only when a verified issuer exists in the same organization. Reading (export, detail, agent list) never persists anything; the resolved chain is written only when an import or adoption stores a certificate body that has no chain. API additions are additive: chain_source, chain_complete, chain_warnings on GET /certificates/{id} (and /projects/{id}/certificates/{certId}); certificate_pem, chain_pem, chain_certificates on GET /discovered-certificates/{id}; export of a record without a body now returns 422 CERTIFICATE_BODY_MISSING instead of a generic 500. Terraform: mazevault_certificate data source gains chain_pem, chain_source, chain_complete; its certificate_pem is now populated (it was always empty before because the SDK read a field the API does not emit) — plans that referenced it will show a value for the first time. Behavioral changes to review: (1) a private key import for a managed certificate without a body is rejected until the body exists (the next agent scan, the next CA synchronisation, or a manual import of the public certificate — CRT/PEM/DER or PFX — attaches it; agents still using the legacy /agents/me/discovery protocol do not report the body, so records discovered only by such agents are completed by one of the other two paths); (2) deployment targets are validated when saved and when the rotation workflow is built: WebLogic targets need keystore_path, a trust_store_password_ref when trust_store_path is set, and wlst_script_path for any restart mode; database wallet targets need an agent, wallet_path, an Oracle provider and update_tns_names=false (rac_nodes is refused as well); web_server and load_balancer targets are refused. Existing targets are not modified — an enabled target that violates these rules now fails its rotation with the target ID and reason instead of being skipped or reporting false success, and it can always be disabled or deleted; the UI editor turns a legacy update_tns_names=true off on save and shows the alternative for unsupported providers and target types. Review the deployment targets of production certificates for web_server, load_balancer, non-Oracle Database Wallet entries and wallet targets without an agent before enabling automatic rotation on them; (3) with the agent option install_chain_to_trust_store enabled, an intermediate CA that previously had no chain will now also install its root into the host trust store on deployment — the same behavior PFX imports with an embedded root always had. Agent rollout: the backend/gateway fix for JKS / PKCS#12 / Windows My keystore deployment works with already-deployed agents. The Agent File (PEM) target, agent_command steps (WebLogic restart, Oracle orapki), the public-only semantics of windows_root / windows_ca, and Oracle wallet deployment require the 1.0.61 agent; older agents answer those requests with an explicit unsupported proxy message type error and the deployment step fails fast. Oracle and WebLogic deployments are covered by contract tests against the agent protocol — validate them against a real orapki / WLST host on staging before relying on them in production. The unreachable duplicate discovery-adoption code path (CertificateDiscoveryService, handlers_discovery.go) was removed; its list filters (managed_certificate_id, discovered_by_agent_id, expiry) were merged into the live GET /discovered-certificates endpoint as the query parameters agent_id, managed_certificate_id and expiring_within_days, and POST /discovered-certificates/{id}/ignore now accepts an optional {"reason": "..."} body that is stored as state_reason and recorded in the audit log — the only two capabilities the removed code had that the live path lacked; adoption, bulk adoption and the detail enrichment were already a superset there. Both live endpoints gained Swagger documentation, and the five never-routed /discovery/certificates* paths disappeared from the Swagger output. There are no new environment variables, Helm values, or release-package format changes.
  • Upgrade Impact (Certificate Key Custody, CSR Validation, Serial Normalization) — No database migration, no new environment variable, no Helm value, no release-package format change, and no Terraform provider contract change (the mazevault_certificate resource is CSR-based and immutable; attaching a key is an operator action). API additions are optional and additive: expected_certificate_id on POST /certificates/import and POST /certificates/import/file, key_source_file in the preview-multi response, identity_normalized in the POST /admin/certificates/backfill response, and the failed value in the certificate-request status enum. Behavioral changes to review before upgrading a production inventory: (1) templates with a non-empty allowed_key_sizes now reject uploaded CSRs with other key sizes at submission — including for ADCS and external CAs, which previously did not check — so verify the list on templates used with customer-supplied CSRs (an empty list means any size); (2) templates with an explicit san_config now enforce it on uploaded CSRs — allow_additional_sans and max_san_count columns are deliberately not used for uploaded CSRs because the template editor never persisted them reliably; (3) on primary startup a background, idempotent pass rewrites serial_number / thumbprint_sha256 / thumbprint_sha1 only for rows that diverge from their stored DER (hex serials written by ACME/Vault/external-order finalization, uppercase thumbprints); internal-CA, ADCS, DigiCert, Venafi and imported certificates are already canonical and are not touched, CA/CA-account bindings and renewal settings are never modified, and no sync events are emitted; (4) importing a certificate that already exists in another project now returns an error instead of silently attaching material to the other project's record; (5) auto-approved CSR requests whose issuance fails are marked failed (externally offloaded pending keys are intentionally retained with their reference for operator cleanup — nothing is deleted from a secret manager on a CA failure), and manually approved failures return the request to pending. Deferred decisions (documented in the architecture reference): a unique index on certificates.thumbprint_sha256 (pending external orders and legacy duplicates would break upgrades), enforcing template-to-project assignment on CSR submission (requires a MAJOR bump for API/Terraform clients), custom EKU OIDs in the internal CA, and a retry cap / denied-state detection for ADCS orders that stay pending indefinitely.
  • Upgrade Impact (Rotation Templates) — Version 1.0.61 adds one idempotent migration, 000195_rotation_template_policy_fields, which adds three columns to rotation_config_templates: lead_time_days (INT, default 0), grace_period_days (INT, default 0), and secret_custody_mode (VARCHAR, default local). The database health check now validates the new template policy columns. The mazevault_rotation_template Terraform resource gains the optional lead_time_days, grace_period_days, and secret_custody_mode arguments, and the Go SDK is updated to match; all additions are additive and backward compatible, with no removed or renamed resource, data source, API endpoint, or SDK contract. There are no new environment variables, Helm values, or release-package format changes. The provider and SDK additionally expose the already-persisted project_id and retry_delay_seconds fields (no migration), and organization_id on the resource is Optional+Computed. Caveat: on template create, an explicit 0 for max_retry_attempts, retry_delay_seconds, or timeout_minutes is not honored — the server applies its defaults (3/5/10); omit those fields or set a positive value (changing them to 0 later via update is honored).
  • Upgrade Impact (Agent Lifecycle) — This feature adds one idempotent migration, 000196_agent_lifecycle_metadata, which adds nullable lifecycle bookkeeping columns to agents (unregistered_at, unregistered_by, lifecycle_reason, lifecycle_reason_category, identity_revoked_at, uninstall_status, uninstall_requested_at, uninstall_completed_at, replaced_by_agent_id); the database health check validates the new columns. A second idempotent migration, 000198_deployment_purpose, adds a purpose column (VARCHAR, default standard) to deployments so an authorized single-use replacement token can be distinguished from a standard enrollment token — only a replacement token may re-enroll a retired agent; the database health check validates this column too. All additions are additive and backward compatible. The RequireActiveAgent guard on JWT-authenticated agent routes means agents that have been unregistered, revoked, or deleted are now rejected even while holding an unexpired token; re-enrollment through a valid deployment (or single-use replacement) token remains the supported recovery path — older agents remain compatible because they simply re-register when rejected. Four additive agent:admin API endpoints are introduced (/agents/:id/lifecycle-impact, /unregister, /reregister, /uninstall) plus the additive DELETE /api/v1/deployments/:id (deployment:write). Deployment list responses now omit the bootstrap token (DeploymentResponse), while the create response continues to return it once (DeploymentCreatedResponse) — the Go SDK and the mazevault_deployment Terraform resource are unchanged in contract and still receive the token on create. The Terraform provider's mazevault_deployment delete now calls the new delete endpoint (previously a state-only no-op), so terraform destroy invalidates the enrollment token server-side; the resource Read also repopulates organization_id so an imported deployment can be destroyed. Registered agents are unaffected. Remote uninstall is opt-in and only attempted for connected Linux packaged installs. There are no new environment variables, Helm values, or release-package format changes for this feature.
  • Upgrade Impact (Security & Dependencies) — These are dependency and build-time changes only: no database migrations, environment variables, Helm values, or release-package format changes, and no API, Go SDK, or Terraform provider contract changes.
  • Upgrade Impact (Frontend MUI v9 & Node toolchain) — The Material UI v9 upgrade is a frontend-internal change with no change to application routes, behavior, backend/API/Go SDK/Terraform contracts, environment variables, Helm values, or release-package format. Operators building the frontend from source now need Node ≥ 22.22 (CI and the frontend container image already run Node 24; .nvmrc pins Node 24 for local development). Runtime deployment of the pre-built frontend image is unaffected.
  • Upgrade Impact (Rotation Backups) — This change adds one idempotent migration, 000197_rotation_backup_lifecycle, which adds status (VARCHAR, default pending), acknowledged_at, and acknowledged_by columns to rotation_backups, backfills status from restored_at, and replaces the old expiry index with a purge-oriented partial index plus a status index; the database health check validates the new columns. One new optional runtime environment variable is introduced — MAZEVAULT_ROTATION_BACKUP_RETENTION_DAYS (default 7) — propagated across .env.example, docker-compose, installers, and all Helm values. Four additive rotation API endpoints are added under /rotation/backups (list rotation:read; restore rotation:execute; reveal/acknowledge rotation:write, with reveal/restore additionally enforcing secret:read). All changes are additive and backward compatible — no removed or renamed resource, data source, API endpoint, or SDK contract — and there are no release-package format changes. Because the backup insert path now succeeds, the pre-rotation backup and automatic-rollback machinery become active for the first time; backups needing manual recovery are surfaced via the new endpoints and the mazevault_rotation_backups_recovery_required metric.
  • Upgrade Impact (Enterprise Functions / Deployment Gating) — This is a UI-reorganization and license-enforcement change only: no database migration, no environment variable, no Helm value, and no release-package format change. A new license feature key, deployment_management, is added to the PoC and Enterprise feature sets derived by getLicenseFeatures (state-aware: active / grace / expiring), and FeatureLicenseMiddleware is applied to the /api/v1/deployments CRUD group. No API route, request/response schema, Go SDK, or Terraform provider contract changes, and the Swagger surface is unchanged. Behavioral change: tenants on a Small Business or unlicensed plan that previously reached the Deployment CRUD API now receive HTTP 403 and an upgrade banner; PoC and Enterprise tenants are unaffected. The unauthenticated UUID-protected agent bootstrap endpoints remain ungated, so agent installation is not affected. The upgrade-banner copy for SSH, Configuration, and KeyTab was aligned to read "PoC or Enterprise".
  • Upgrade Impact (Orchestrator Mode Key Offload) — Backend-only change: no database migration, no new environment variable, no API contract change. Two behaviors are intentionally stricter and should be reviewed by operators running Orchestrator Mode: (1) Integration lookup no longer widens beyond the caller's scope. A token, keytab or private-key offload that carries a project/organization scope and finds no enabled secret_manager / key_storage / hsm integration in that scope now fails with no enabled key storage integration found for project/organization … instead of silently using the first integration anywhere in the system; an explicitly selected integration_id that does not exist or is disabled is rejected the same way. Make sure every organization that uses managed tokens or keytabs in Orchestrator Mode has an enabled secret-manager integration in one of its projects. (2) Azure Key Vault size limit is enforced before the write. Values whose base64 transport encoding exceeds 25 600 bytes (≈19 200 raw bytes) are refused with 400 and the exact limit; this is Azure's documented per-secret cap, not a MazeVault restriction — larger values require a HashiCorp Vault or AWS Secrets Manager integration (64 KB) as the project's key-storage target. Secrets created in Key Vault by this release use the new flat naming (mazevault-token-…, mazevault-keytab-…, mazevault-<cn>-<serial>); existing references stored on certificates are honored unchanged (update_version rolls a new version of the same secret). Nothing is renamed or migrated in Key Vault.

Version 1.0.60

Release Date: 2026-07-20

New Features

  • Token Management — External API Tokens with Mandatory Expiry Tracking and Agent Deployment — A new Enterprise function (PoC or Enterprise license, feature token_management) for managing externally-issued API tokens — for example Jira or Signi tokens — that can only be renewed manually in the source system's own UI. Each token is stored with a mandatory expiration date and an optional per-token alert lead time (default 30 days); MazeVault raises a token_expiry incident through the existing notification channels (email, webhook, Teams, Slack, Jira, SMSEagle) before the token lapses, and escalates it to critical once expired. Token values are encrypted at rest (AES-256-GCM) in standard mode, or offloaded to the configured external secrets manager (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault) in Orchestrator Mode. A token can optionally be bound to a project and deployed to the same target types as secrets, reusing MazeVault's shared value-delivery executors: an agent-managed server configuration file (JSON/YAML/ENV key→value, with an optional allowlisted post-install command), a Kubernetes secret, an Azure Key Vault secret, or an AWS Secrets Manager secret (Windows registry / IIS targets are intentionally deferred). A manual renew action records the newly obtained value and its new expiry, resolves the open expiry incident, and re-deploys any targets flagged for sync-on-renewal. Token Management is deliberately separate from Project Key management (project secrets) and Organization System Keys (MazeVault infrastructure keys), and appears under a new Enterprise Functions navigation group. Access is governed by new token:read / token:write / token:delete / token:admin permissions — deployment and renewal require token:write (admin implies it), and revealing a stored token value requires token:admin. The feature is fully manageable through the new mazevault_token Terraform resource and mazevault_tokens data source.
  • Upgrade Impact (Token Management) — Adds two idempotent migrations: 000193_token_management (creates managed_tokens and token_deployment_targets, seeds token:* RBAC permissions onto existing roles — org_admin full; operator read/write/delete; user and auditor read-only) and 000194_token_target_types (generalizes token_deployment_targets with target_type / integration_id / config so token delivery reuses the shared secret/certificate executors). There are no new environment variables, Helm values, or release-package format changes — the feature reuses the existing encryption, secret-offload, notification/incident, rotation-step delivery, and license infrastructure. The database health check now validates the two new tables. The Terraform provider gains the additive mazevault_token resource and mazevault_tokens data source; no existing resource, data source, API endpoint, or Go SDK contract is changed or removed. The endpoints are gated by the token_management license feature (PoC and Enterprise); without it the /api/v1/tokens API returns HTTP 403 and the UI shows an upgrade banner.

Improvements

  • Admin Batch Triage on Dedicated Rotation and Synchronization Problem Pages — System administrators can now select individual rows or all currently visible rows on Rotation Problems and in each section of Synchronization Problems. The frontend deliberately replays the existing audited single-item actions with at most three requests in flight, reports progress and partial failures, and refreshes the affected problem lists after completion; no unaudited bulk mutation path is introduced. Rotation failures are acknowledged and their linked open incident is resolved when available. Synchronization failures and blocked-seed incidents are acknowledged in their own sections, while pending synchronization conflicts can only be batch-ignored, which changes local conflict status without writing either value to an external provider. “All visible” is intentionally page-bounded and every confirmation states the exact affected count. Individual remediation remains available to non-admin operators with the existing write permissions; batch controls are shown only to the system administrator.
  • HashiCorp Vault Transit HSM/KMS Backend — mazevault_hsm_device now supports provider_type = hashicorp_vault, backed by the HashiCorp Vault Transit secrets engine. Previously the value was accepted by validation but instantiation failed with unsupported HSM provider: hashicorp_vault because no implementation existed. Vault Transit keys are generated and held by Vault (RSA-2048/3072/4096, ECDSA P-256/P-384/P-521); signing is performed remotely via the Transit sign endpoint so private key material never leaves Vault. The device connectivity test, key listing, public-key retrieval, remote signing (used by internal CA signing and other HSM-routed operations), and key deletion are all implemented. Configuration takes vault_url, token, and an optional mount_path (default transit). Key-encryption-key (KEK) wrapping remains served by the separate system HSM interface and is out of scope for this provider.
  • Provider-Aware Certificate Templates for External Commercial CAs — The certificate template form now adapts to the capabilities of the selected issuing CA account's provider instead of showing one fixed field set for every authority. Each external CA has a different API and issuance model, so the form is driven by provider-declared capabilities (exposed on GET /ca-providers/{type}/schema): providers with a product catalog (SSLMarket, DigiCert) present the certificate product as a dropdown from the CA's synced product list — the certificate type is derived from the selected product and the free-text external-template OID is hidden; providers that require domain control validation expose a per-template Domain Validation Method (email / dns / file), stored on the template as dcv_method (migration 000191); ACME issuers continue to show the ACME challenge policy; internal CAs show neither. The SSLMarket provider now resolves the product code from the template's linked CA product (falling back to a free-text code and then the account default) and honors the per-template domain validation method; DigiCert likewise prefers the per-template method. A new optional dcv_method argument is available on the mazevault_certificate_template Terraform resource. Existing templates are unaffected: the new column is nullable and empty means "use the provider/account default".

Bug Fixes

  • Terraform Managed Tokens Now Have a Valid, State-Safe Lifecycle — The mazevault_token resource used the reserved root attribute name provider, causing Terraform schema validation to reject the resource. The label is now exposed as token_provider, with a versioned state upgrader that preserves existing state values. Token reads and deletes now treat HTTP 404 as resource absence while preserving state on authorization, transport, and server failures. Empty description / project_id responses no longer leave stale non-empty state. New tokens can use Terraform 1.11+ value_wo with value_wo_version, so plaintext is absent from plan and state; legacy value remains supported but is explicitly documented as stateful. Import now supports metadata-only management. Migrating an existing resource from value to value_wo is intentionally not automatic because replacement changes the token ID and cascade-deletes its deployment targets.
  • Rotation Failure Triage Shows the True Open Total with a Rolling Ten-Item Queue — The Overview widget previously displayed the number of incidents in the API's default 20-row page as if it were the total, so tenants with more than 20 open rotation failures always saw an incorrect count. The widget now uses the existing paginated incident response's authoritative total, requests only the ten newest unresolved rotation-failure incidents for display, and reloads the first page after each individual close so the next unresolved incident immediately fills the queue. The misleading page-local High/Critical badge has been removed. Overview intentionally remains an individual-triage surface with no selection or batch-close action.
  • Secrets Dashboard "Missing in PROD" Metric Now Correctly Reflects Cross-Environment Parity — The Secrets Dashboard KPI previously labeled Missing in PROD never inspected production environments or the is_production organization setting. It counts secret keys that exist in at least one active (comparison-enabled) environment but are missing in another active environment — a cross-environment parity signal that requires at least two active environments. On instances with no production environment (for example a test tenant with only development, integration, migration, and testing), this produced a confusing non-zero "Missing in PROD" count. The card is now labeled Missing (Cross-Env) on both the Secrets Dashboard overview and the Reports view, matching the existing Missing Secrets (Cross-Environment) table below it. The underlying cross-environment computation is unchanged — only the misleading naming and label were corrected.
  • Orchestrator Mode Key Custody Events Persist Reliably — In Orchestrator Mode, offloading a private key (for example a certificate key sent to Azure Key Vault) recorded a custody audit event whose metadata value was an empty string. Because key_custody_events.metadata is a PostgreSQL JSONB column, the insert failed with invalid input syntax for type json (SQLSTATE 22P02) and the custody entry was lost — logged only as a warning — even though the key offload itself succeeded. Custody events now always persist syntactically valid JSON: an unset or blank metadata value is normalized to an empty JSON object ({}) at the write boundary, so every key offload, retrieval, and deletion produces a durable custody audit record. Callers that already supply structured metadata are unaffected. The database health check now also validates the key_custody_events columns.
  • SSLMarket Certificate Issuance Corrected to the Live SSLmarket API — Issuing a certificate through an SSLMarket CA account failed with API request failed with status 400: {"error":["No route found"]} on both standard and Orchestrator Mode instances. The SSLMarket provider had been implemented against a REST shape that does not match the published SSLmarket API. The provider now targets the real endpoints and payloads: orders are submitted to POST /order/certificate/{product_code} with the flat order body (domain, SANs, years, CSR, the mandatory dv_auth_method, and owner/authorized-contact/technical-contact/invoice fields that fall back to the SSLmarket account defaults when not configured); order detail, certificate download (by order id and format), domain-control-validation tokens, and revocation now use the correct routes; product resolution reads the real {"products":[…]} catalog; and responses are parsed with the API's actual JSON shapes (string identifiers, array dns_names, and the paid/requested order states). Authentication (X-Auth-Token) and the base URL were already correct and are unchanged. Certificate issuance and the asynchronous external-CA order poller now complete against SSLMarket.
  • Orchestrator & Sync Secret-Push Audit Events No Longer Fail with a Foreign-Key Violation — In Orchestrator Mode, pushing a secret to an external provider (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault) recorded a SECRET_PUSH_CREATE / SECRET_PUSH_UPDATE audit event whose user_id was mistakenly set to the project's ID instead of a user. Because audit_events.user_id has a foreign key to users(id), the insert failed with violates foreign key constraint "audit_events_user_id_fkey" (SQLSTATE 23503) and the audit record was lost even though the external push itself succeeded — so the external secret diverged from the audit trail. Secret-push and sync-seed audit events are now attributed to the initiating user (and, for system-initiated syncs, to the resolved system actor), so they persist correctly. As defense-in-depth, the audit writer now also guarantees durability: if any audit event ever fails to persist because of a bad actor reference, it is retried once with a null (unattributable) actor rather than dropped; only if the database is entirely unreachable is the full event emitted to the central structured log as a last resort. The previous local emergency-log file sink (/var/log/mazevault/emergency_audit.log), which was unusable on read-only container filesystems in Kubernetes, has been removed. A new mazevault_audit_persist_failures_total metric makes any last-resort emission alertable.
  • Merkle Integrity Cache Now Tracks Secret Changes — The incremental Merkle integrity cache recognized only the certificate/keytab event vocabulary (create / update / delete) and silently ignored the secret service's past-tense events (created / updated / deleted), logging merkle cache updater: unknown event type and leaving the secret integrity cache stale after every secret change. The cache updater now accepts both forms, so secret create/update/delete operations correctly update the Merkle integrity tree.
  • Rotation Backups Are Now Encrypted and Support Automatic Rollback — The rotation orchestrator was started without its encryptor wired in, so pre-rotation backups degraded to non-restorable version references and logged rotation backup requested but encryptor not configured — rollback will be limited. Automatic rollback of a failed rotation was therefore unavailable. The orchestrator now receives the system encryptor at startup, so the pre-rotation state is captured as an encrypted backup and automatic rollback is restored. Backups for external names_only secrets continue to rely on the external provider's own version history, as before.
  • Entra ID Integrations Accept AKS Workload-Identity Background Authentication — Configuring an Entra ID integration (used for app-registration secret rotation and Microsoft Graph sync) with background_auth_method=workload_identity was rejected with unsupported background_auth_method, even though the Microsoft Graph client and the shared Azure credential factory already support AKS federated workload identity. Three separate validation gates — integration create/update, test-connection, and the runtime consistency check used by rotation — defaulted to rejecting the value, so the integration could not be created or, if forced, failed later during rotation with configuration is inconsistent. All three gates now accept workload_identity, which — like managed_identity — requires no static credentials in the integration config: on a workload-identity cluster the runtime performs the federated token exchange for the configured (or webhook-injected) managed-identity client ID. This unblocks Entra credential rotation on AKS workload-identity clusters without a dedicated client-secret app registration.
  • Spring Actuator Refresh Rotation Step No Longer Crashes on Malformed Configuration — The spring_actuator post-rotation step used unchecked type assertions on actuator_url and on entries of the optional endpoints list, so a wrong config key (for example url instead of actuator_url) or a non-string endpoint caused a worker panic instead of a clear error. The step now validates its input defensively and returns a structured error, and it builds the refresh URL safely so a base URL, a trailing slash, or a URL that already ends in /actuator or /actuator/refresh never produces a doubled path.
  • Post-Rotation Secret Delivery to Key Vault and Other Targets Now Receives the Rotated Value — Post-rotation delivery actions (Azure Key Vault push, AWS/GCP/OCI secret managers, DevOps variables) require the freshly rotated secret value, but the post-rotation action phase rebuilt an execution context that omitted it, so a delivery action logged new_password not found in input context and silently skipped the write. The rotated value is deliberately never persisted, so it is now resolved at execution time from the just-updated MazeVault secret — identically whether the action runs on the primary or is delegated to a gateway (the value is fetched by the executing runtime and is never carried in the gateway task payload). Delivery for Entra credential rotations continues to flow through the dedicated seeded delivery workflow. As a hardening measure, the previous Key Vault value captured for rollback is now redacted from the persisted post-action record so no secret material is written to the rotation history.
  • Azure Managed HSM / Key Vault HSM Devices Can Now Be Tested and Used — Instantiating an azure_managed_hsm HSM/KMS device — for the device connectivity test as well as any CA signing, key offload, key rotation, or secret encryption routed through it — failed with CredentialFactory is required for AzureKVProvider because the HSM device service constructed the Azure provider without a credential factory. The Azure credential factory is now injected into the HSM device service and threaded through the provider factory, so Azure Managed HSM devices initialize with a real Azure credential.
  • Manual Secret-Manager Integration Sync No Longer Rejects Non-Canonical Type Values — Triggering a manual synchronization on a secret-manager integration could fail with sync not supported for integration type: secret-manager when the integration's stored type used a hyphen instead of the canonical underscore form. Integration types are now canonicalized (lowercased, trimmed, hyphens folded to underscores) when an integration is created, and the manual-sync guard compares against the canonical form, so secret-manager and secret_manager are treated identically. Migration 000192 normalizes any existing non-canonical project_integrations.type values so exact-match checks (manual sync, sync rules, key offload) stay consistent.
  • Pull/Bidirectional Sync Rules Require a Target Environment Up Front — Creating a pull or bidirectional sync rule without a target_environment was accepted but later failed at execution deep in the environment canonicalizer with environment slug is empty. Sync-rule creation now requires and canonicalizes target_environment for any rule that is not a pure push (push rules derive the environment from the source secret), returning a clear validation error instead of a confusing runtime failure.
  • ACME Certificate Issuance Now Completes Finalization (Let's Encrypt / ACME CAs) — A v1.0.59 report described ACME issuance passing dns-01 validation but failing at finalize with the error POST-as-GET requests must have an empty payload / must have a nil body, and this was reproduced end-to-end against a local Pebble server. Two defects were found and fixed. First, MazeVault submitted the CSR to the order URL (order.URI) instead of the order's dedicated finalize URL (order.FinalizeURL); because the order URL is a read-only POST-as-GET resource, posting the CSR body there was rejected by the CA. Second, once the CSR reached the correct finalize endpoint, some RFC 8555-compliant servers (including Pebble) do not return a Location header on the finalize response, so the underlying ACME client's internal post-finalize order poll targeted an empty URL and failed even though the CSR had been accepted; issuance now recovers by polling the known order URL and fetching the issued certificate. End-to-end dns-01 issuance through finalize is verified by a Pebble-backed reproduction test (build tag acme_pebble).
  • Upgrade Impact — Version 1.0.60 is a naming and label correction with no database migrations, environment variables, Helm values, or release package format changes. In the dashboard secrets overview endpoint (GET /api/v1/dashboard/secrets/overview) the response field missing_in_prod is renamed to missing_cross_env. This is an internal dashboard field consumed only by the MazeVault frontend; it is not part of the Go SDK or the Terraform provider, so no integration contract is affected. The cross-environment parity logic and the missing-secrets listing are functionally unchanged.
  • Upgrade Impact (Orchestrator custody & SSLMarket) — The Orchestrator Mode key custody persistence fix and the SSLMarket provider correction are backend-only changes with no database migrations, environment variables, Helm values, or release package format changes. To issue through SSLMarket, the CA account integration must set the correct product_code (an SSLmarket account may offer products — such as trial certificates — that are not returned by the API product catalog and therefore must be configured explicitly) and, when the account has no default business data, the owner/contact/invoice settings required by the chosen product. dv_auth_method defaults to Email when not configured.
  • Upgrade Impact (Provider-aware templates) — The provider-aware certificate template feature adds one nullable column via migration 000191 (certificate_templates.dcv_method) and one runtime-computed field (template_form) on the GET /ca-providers/{type}/schema response; both are additive and backward compatible. No environment variables, Helm values, or release package format changes. The mazevault_certificate_template Terraform resource gains an optional dcv_method argument; existing configurations continue to work unchanged.
  • Upgrade Impact (Audit, Merkle & Rotation) — The secret-push audit attribution fix, the audit durability hardening, the Merkle integrity cache fix, and the rotation backup encryption fix are backend-only reliability changes with no database migrations, environment variables, Helm values, or release package format changes. One additive Prometheus counter, mazevault_audit_persist_failures_total, is exposed; no existing metric, API endpoint, Go SDK, or Terraform provider contract changes. The local /var/log/mazevault/emergency_audit.log fallback is removed in favor of the central structured log, so the last-resort audit sink now works on read-only container filesystems.
  • Upgrade Impact (Entra WI, Rotation Delivery, HSM & Sync Validation) — These fixes add one idempotent data migration, 000192_canonicalize_integration_type, which normalizes existing project_integrations.type values to their canonical lowercase-underscore form; it is data-only and backward compatible. There are no new environment variables, Helm values, or release package format changes. background_auth_method=workload_identity is now an accepted value for Entra integrations (including via the mazevault_integration API surface), and pull/bidirectional sync rules now require target_environment at creation time. No existing API endpoint, Go SDK, or Terraform provider contract is removed or renamed.
  • Upgrade Impact (Vault HSM & ACME finalize) — The HashiCorp Vault Transit HSM provider and the ACME finalize fix are backend-only with no database migrations, environment variables, Helm values, or release package format changes. The hashicorp_vault HSM device provider is additive; no existing HSM device configuration changes. The ACME finalize fix corrects the certificate issuance flow only; no API endpoint, Go SDK, or Terraform provider schema is added, removed, or renamed. The Pebble reproduction test is gated behind the acme_pebble build tag and does not run in normal CI.
  • Upgrade Impact (Problem Triage UX) — The truthful Rotation Failure Triage total and the administrator batch workflows are frontend-only changes. They reuse the existing incident pagination metadata and existing single-item mutation endpoints, permissions, and audit events. There are no database migrations, API or Swagger contract changes, new RBAC permissions, environment variables, Helm values, release-package changes, Go SDK changes, or Terraform provider changes.

Security & Dependency Updates

  • Trivy CRITICAL/HIGH Dependency Vulnerabilities Cleared Across All Modules — The dev-pipeline Trivy filesystem scan (CRITICAL,HIGH, --ignore-unfixed) was failing on fixed-but-outdated dependencies. All affected packages were upgraded to their fixed versions and verified with go build, go vet, module unit tests, and govulncheck (0 reachable vulnerabilities), plus a frontend build and the full Vitest suite:
    • Go — golang.org/x/text 0.37.0 → 0.39.0 (CVE-2026-56852 / GO-2026-5970, norm.Iter infinite loop) in backend, cli, k8s-operator, and terraform-provider-mazevault.
    • Go — google.golang.org/grpc 1.79.3 / 1.80.0 → 1.82.1 (GHSA-hrxh-6v49-42gf / GO-2026-6061, xDS RBAC and HTTP/2 transport) in backend and terraform-provider-mazevault.
    • Go — software.sslmate.com/src/go-pkcs12 0.7.0 → 0.7.2 (GO-2026-5052) in backend and agent; this reachable vulnerability was surfaced by govulncheck even though Trivy's severity filter did not report it.
    • Frontend — axios 1.16.0 → 1.19.0 (GHSA-gcfj-64vw-6mp9 and related advisories).
  • React Router Upgraded to v8 (Major) — The frontend was migrated from react-router-dom 7.17.0 to react-router 8.3.0, which resolves CVE-2026-55685 and GHSA-qwww-vcr4-c8h2. React Router v8 removes the separate react-router-dom package; all imports were moved to react-router (the app is a declarative SPA using BrowserRouter, so no react-router/dom entry point is required). react and react-dom were raised to a minimum of 19.2.7.
  • Upgrade Impact (Security & Dependencies) — These are dependency and build-time changes only: no database migrations, environment variables, Helm values, or release-package format changes, and no API, Go SDK, or Terraform provider contract changes. The frontend build now requires Node ≥ 22.22 (declared via a new engines field in frontend/package.json); the CI frontend jobs and the frontend Docker image already run on Node 24. The React Router v8 upgrade is a frontend-internal migration with no change to application routes or behavior; the full frontend unit-test suite passes unchanged.

Version 1.0.59

Release Date: 2026-07-18

Improvements

  • Secret Rotation Views Refresh Silently After Background Completion — The Secrets tab now uses the existing per-secret rotation status poller as an event detector. When a background secret rotation reaches a terminal state, MazeVault refreshes only the affected secret through GET /api/v1/secrets/{id} and updates the visible version counter without reloading the whole secrets table. If the Version History or Rotation Configuration modal is already open for that secret, the modal reloads its version list or execution history in place. No extra global polling, toast, highlight, or full-page refresh is introduced.
  • Dedicated Access Control for Synchronization Problem Remediation — Synchronization problems now use a dedicated sync:read / sync:write permission boundary instead of implicitly tying action buttons to project:write. The project_admin, certificate_manager, and secret_manager roles can actively remediate synchronization problems, while auditor and regular users remain read-only. Navigation and the Synchronization Problems page now reflect the user's actual sync permissions.
  • Sync Failure and Blocked Seed Actions Are Available Directly from the Problem View — The global Synchronization Problems page and the project-level sync conflicts tab now provide confirmed actions for retry, conflict ignore, current sync failure fingerprint acknowledgement, and sync_seed_blocked incident acknowledgement. The left navigation badge now uses the aggregate sync_problem_count, covering conflicts, current sync failures, and blocked seed incidents.
  • Synchronization Problem Dismissals Scale with One Batched Lookup — The sync failure listing now evaluates acknowledged current-failure fingerprints with a single batched dismissal query instead of one lookup per failed sync rule. This preserves the existing rule/timestamp/error fingerprint semantics and soft-delete behavior while keeping the Synchronization Problems view responsive as the number of failed rules grows.
  • Certificate Exports Include the Issuer CA Chain More Reliably — Certificate export now normalizes stored certificate bundles and, when needed, resolves the issuing internal CA hierarchy through parent CA records. PEM/CRT and PKCS#7 exports include the leaf certificate followed by the issuer chain, while DER remains leaf-only as before.
  • Certificate Renewal Readiness Understands Internal CA Accounts — Renewal eligibility, rotation preflight, and rotation resource metadata now resolve internal organization CA accounts through their backing CertificateAuthority. Internal CA-backed renewals report both the CA account and the underlying signing CA, matching the runtime renewal path.
  • External CA Domain Validation Is Now Visible and Durable — External CA orders from DigiCert and SSLMarket now expose DCV instructions through registered renewal queue DCV endpoints and the project certificate table. Certificates with pending domain validation show a View Domain Validation action that opens the existing DCV status panel, where operators can inspect DNS/HTTP/email validation details and trigger provider re-checks without leaving the certificate workflow.
  • New External CA Certificate Requests Can Wait for Provider Validation — Certificate requests issued through an external CA no longer assume that SignCSR returns a certificate immediately. If the provider returns an order ID without PEM, MazeVault stores a pending certificate placeholder, links it to the request, records a durable submitted_to_ca queue item, and lets the external CA order poller complete the request when the provider later makes the certificate available.
  • CA Account Provider Settings Are Persisted and Terraform-Manageable — CA account create/update/sync flows now preserve non-secret provider settings such as DigiCert organization_id, server_platform_id, dcv_method, certificate_dcv_scope, custom base_url, and SSLMarket defaults in account metadata. The Terraform mazevault_ca_account resource adds a settings map for those non-secret values while keeping API keys and ACME EAB material in write-only credentials.
  • Terraform Secrets Support Write-Only Values for Ephemeral Passwords — The mazevault_secret resource now supports value_wo and value_wo_version for Terraform 1.11+ write-only inputs. Operators can generate a password with an ephemeral Terraform resource such as ephemeral "random_password" and pass it to MazeVault without storing the plaintext in Terraform plan or state. value_wo_version is the stored resend trigger: increment it when Terraform should send a new write-only value. The legacy value argument remains supported for existing configurations, and server-side generate remains available for MazeVault-generated secrets.
  • Secret Update API and Go SDK Support Metadata-Only Updates — PUT /api/v1/secrets/{id} now treats value as optional: omitting it updates metadata and TTL only, while supplying a non-empty value continues to create a new secret version. The Go SDK preserves the existing UpdateSecret(id, value, ...) method and adds a request-based update path that can intentionally omit value, which the Terraform provider uses for write-only secret lifecycle management.

Bug Fixes

  • Synchronization Problems No Longer Lack Action Icons for Authorized Roles — Users who could see problems through sync:read previously often had no way to resolve them because mutations were bound to project:write. The new sync failure and blocked seed acknowledgement endpoints are protected by sync:write, audited, and intentionally limited to the synchronization problem surface.
  • Ignoring a Sync Conflict No Longer Requires an External Provider Write — The ignore resolution now closes the local sync_conflicts row with status ignored and does not write to the target secret manager. This allows operators to permanently defer a known conflict without risking an unintended external value change.
  • Sync Failure Acknowledgement Applies Only to the Current Failure — Sync failures derived from sync_rules.last_sync_status are dismissed by a fingerprint composed from the rule, last sync timestamp, and error. If a later run fails differently, the problem appears again; acknowledgement therefore does not hide new or changed failures.
  • Rotation Configuration Edit No Longer Shows a Spurious Partial-Load Warning — The secret rotation configuration modal now addresses rotation history and readiness by the canonical rotation resource id (rotation_configs.id / resource.resource_id) instead of the secret id. This fixes the misleading "Some rotation sections could not be loaded. Available data is shown." warning when opening Secrets → Rotations → Edit for a configured secret.
  • Secret Version Rollback Works Reliably for Existing Version History — Secret rollback now stores archived version rows with a generated primary key, preserves the original version timestamp instead of stamping the archive time, and updates rollback metadata through the model serializer path. The frontend also surfaces backend rollback message / error details as an error notification instead of replacing them with a generic "Rollback Failed" response.
  • Legacy Secret Version Rows with Missing UUIDs Are Repaired — Migration 000189_fix_secret_version_null_uuid assigns valid UUIDs to legacy secret_versions rows that had missing or zero IDs, and the SecretVersion model now generates an ID before insert when one is absent. This prevents primary-key collisions and rollback failures on environments that already contain affected history rows.
  • Manual Secret Rotation No Longer Creates Duplicate Local Versions in Staging — The pre_rotation_sync step now compares the external value with the current local secret value before writing a local update. If the values are identical, the step records the pulled value for downstream rotation context without bumping the secret version. This eliminates the staging symptom where one manual rotation produced two new versions and a confusing intermediate system version.
  • Version History Now Shows the Correct Author for the Current Version — The current (active) version row in the Version History modal was displaying the original creator of the secret instead of the actor who last changed it. The service layer now derives the current-version author from the most recent archived history row, which carries the actor of the last update, exactly as all other history rows do. Automated rotations triggered by the scheduler continue to appear as System; manual triggers show the user who initiated the rotation.
  • Certificate Chain Fields No Longer Duplicate the Leaf Certificate — Issuance, import, renewal, ACME completion, and external CA order completion now normalize certificate PEM data at the write boundary. CertificatePEM remains the current certificate, ChainPEM stores issuer certificates only, and PublicChainPEM stores the full leaf-plus-chain bundle. Existing contaminated chain data is defensively normalized during export.
  • ACME Issuance Stores Leaf and Chain Separately — ACME issuance now splits the returned order certificates into leaf-only certificate data and a normalized issuer chain. This prevents full ACME bundles from being stored twice or treated as both the current certificate and the chain.
  • ACME Bridge Finalization Validates CSR Identifiers and Persists Async ADCS Certificates — MazeVault's ACME server now rejects finalize CSRs whose DNS/IP identifiers are outside the ACME order, uses the RFC 8555 certificate URL path consistently, and creates the tracked Certificate row when an ADCS-backed ACME order completes after CA manager approval.
  • External CA Polling Handles DCV, Pending Orders, and Certificate Downloads Correctly — The external CA order poller now counts only resolved orders as completed work, fetches/stores DCV tokens when a provider reports validation is required, downloads certificate bundles for DigiCert and SSLMarket before finalization, and completes request-originated orders atomically across the certificate, queue, request, and agent assignment records.
  • DCV Retry No Longer Resumes Expired Challenges Prematurely — Operator-triggered DCV retry resets expired or stale challenges before re-fetching provider instructions, handles a missing audit service safely in tests/background contexts, and keeps a queue item in pending_dcv when expired tokens remain instead of incorrectly resuming certificate download.
  • ADCS DCOM Responses Accept Numeric and String Statuses — The backend DCOM parser now accepts both legacy numeric ADCS disposition codes and agent responses such as pending, issued, or denied, preserving existing behavior while allowing the agent/backend contract to converge safely.
  • Renewed Certificates Preserve the Backing Internal Signing CA — Renewals that use an internal organization CA account now preserve the backing CertificateAuthority ID on the renewed certificate while continuing to sign through the configured CA account provider. External CA account renewals continue to leave the internal CA field empty.
  • Approval Continuation Audit Events Carry Rotation Context — Automatic rotation continuation after approval now writes audit events with the rotation intent's project and initiating actor when available, improving traceability without changing continuation idempotency or approval enforcement.
  • Internal CA HSM Algorithm Inference Is Covered by Regression Tests — The internal CA HSM signer now uses a cleaner type switch for RSA, RSA-PSS, and ECDSA algorithm inference, with tests covering the expected JWA algorithm mapping.
  • Terraform Write-Only Secret Updates No Longer Replay or Rotate Values During Metadata Changes — Metadata or TTL updates for a Terraform-managed mazevault_secret that uses value_wo now call the backend without a plaintext value unless value_wo_version changes. MazeVault therefore preserves the existing secret material, version history, offload state, external provider state, and propagation behavior during metadata-only applies. Explicit empty secret values are rejected instead of being confused with omitted values.
  • Terraform CA Account Provider Types Are Validated Consistently — The Terraform mazevault_ca_account resource now rejects empty, whitespace-only, mixed-case, and unsupported provider_type values before any backend API call. The accepted canonical values are aligned with backend CA account resolvers that can actually connect, including internal, the rapidssl SSLMarket alias, and venafi; placeholder I.CA/PostSignum and PSD2 integrations remain excluded until they report a connectable backend status. Venafi account-backed provider resolution is now wired through both CA account service paths, so documented Venafi CA accounts no longer fail in the organization CA-account resolver. Existing documented lowercase configurations continue to work unchanged.

Migration Notes

  • Migration 000189_fix_secret_version_null_uuid repairs invalid secret_versions.id values by replacing missing or zero UUIDs with generated UUIDs. The migration is data-repair only and does not change public API contracts.
  • Migration 000190_sync_problem_actions adds the sync:write permission to eligible system roles and creates the sync_problem_dismissals table for durable acknowledgement of current sync failure fingerprints. The migration is idempotent and does not change configuration schema or environment variables.

Notes

  • Upgrade Impact — Version 1.0.59 contains two database migrations: 000189_fix_secret_version_null_uuid for legacy secret version UUID repair and 000190_sync_problem_actions for sync remediation permissions and the per-fingerprint dismissal table. Sync-specific API endpoints are added for current sync failure acknowledgement and blocked seed incident acknowledgement; they require no new environment variables or release/package format changes. Terraform write-only secret values require Terraform 1.11+ when value_wo is used with ephemeral inputs; existing value and generate configurations continue to work. Secret update now accepts metadata/TTL-only requests without creating a new version, which is an API compatibility expansion. Secret rotation view refresh is frontend-only, and the Version History author fix is a backend service layer change with no configuration impact. The certificate chain, export, ACME, external CA DCV, new-request async issuance, CA account settings, renewal, approval-continuation audit, HSM, SDK, and Terraform provider changes add no additional migrations, environment variables, Helm values, or release package format changes. Terraform CA account provider type validation now rejects invalid values earlier; documented lowercase provider_type configurations continue to work. CertificateRequest.status can now expose submitted_to_ca while an external CA request is waiting for DCV/provider issuance, and the new renewal queue DCV endpoints reuse existing certificate RBAC permissions.

Version 1.0.58

Release Date: 2026-07-16

New Features

  • ACME Challenge Policy on Certificate Templates — Certificate templates now carry a challenge_type field (auto, dns-01, http-01, tls-alpn-01, default auto) that controls how ACME validation is performed when the issuing CA account is an ACME provider (Let's Encrypt, ZeroSSL, custom ACME). The policy is enforced at issuance time in the ACME provider: dns-01 requires a configured DNS solver and fails fast without one, http-01 rejects wildcard identifiers (which ACME cannot validate over HTTP), and tls-alpn-01 returns a clear "not implemented" error instead of silently falling back. auto preserves the previous behavior (wildcard → DNS-01 when a solver exists, otherwise HTTP-01). The field is exposed in the Project Templates certificate wizard, in the certificate template detail views, and via the Terraform provider (mazevault_certificate_template.challenge_type). It is ignored for non-ACME issuers (internal CA, DigiCert, ADCS, …).
  • ACME Account Attributes in the Terraform Provider — The mazevault_ca_account resource now supports the ACME account fields email, directory_url, eab_kid, and eab_hmac_key (the HMAC key is marked sensitive/write-only). These map into the backend's encrypted credential store, enabling Terraform-managed Let's Encrypt, ZeroSSL, and custom/EAB-gated ACME CA accounts. The existing api_key/base_url attributes remain for API-style providers. The provider now sends the backend-compatible credentials/display_name contract instead of the legacy generic config payload, and write-only credentials are preserved on read (no spurious drift).

Improvements

  • Single Internal CA — Coherent Lifecycle and Management — The organization's internal Certificate Authority is now managed as a single, first-class object under Organization Settings → Project Settings → Certificate Authorities → Internal Certificate Authority. New endpoints resolve, initialize, and repair the internal CA (GET/POST /organizations/{id}/internal-ca, POST /organizations/{id}/ca-accounts/{accountId}/initialize-ca), and creating a second internal root is rejected (single-CA guard). The UI provides an Initialize Internal CA action (common name, key size, validity) and an Initialize CA repair action for internal accounts that were previously created without CA key material. OCSP/CRL URLs are editable and CRL regeneration is available directly from the internal CA card. Agents continue to distribute the CA certificate to server trust stores unchanged.
  • "Add CA Account" Is Now External-Only — The Add CA Account flow no longer offers an Internal provider option (which previously created a non-functional "hollow" account with no CA key material). Internal CA management lives exclusively in the Internal Certificate Authority section. The internal CA is filtered out of the external CA-account cards to avoid duplicate representation, while remaining fully selectable everywhere else (project integrations, certificate template CA binding, ACME/EAB dialogs).
  • Strict Renewal Enforcement: Template + CA Required — The certificate renewal pipeline now enforces that every certificate must have a TemplateID and the assigned template must have a CAAccountID before renewal can proceed. Certificates without a template or with a template lacking a CA account are rejected with a clear error message at the pipeline entry point. This eliminates the previous multi-level CA fallback chain (cert-level CA, project overrides, internal CA default) and establishes template.CAAccountID as the single source of truth for CA resolution during renewal.

Bug Fixes

  • Internal CA No Longer Reported as "Not Found" When It Exists — The Certificate Authorities tab previously enumerated project-scoped CAs to display internal CAs, so the organization-level internal CA (which has no project binding) was invisible and the section always showed "No internal CAs found." The section now resolves the internal CA through the organization's internal CA account link and displays it correctly, including its OCSP/CRL configuration.
  • OCSP/CRL Configuration Persists for Org-Level Internal CA — Editing OCSP/CRL URLs for the internal CA now uses the CA-scoped endpoint (PUT /ca/{id}) instead of a project-scoped path that could not address an org-level CA (project_id = NULL). OCSP remains internal-CA-only by design — external CAs (DigiCert, Let's Encrypt, ZeroSSL) continue to advertise their own OCSP/CRL responders via the embedded AIA/CDP pointers.
  • Integration Deletion Impact Now Excludes Soft-Deleted Secrets — The raw SQL aggregates behind affectedSecretCount and collectOrphanedSecretIDs filtered only archived_at IS NULL, ignoring GORM soft-deletes (deleted_at). This could inflate the reported "total affected" / orphaned counts relative to every other code path that queries secrets via GORM (which applies deleted_at IS NULL automatically). Both raw queries now also filter deleted_at IS NULL, restoring a single, consistent secret population across the delete result, the deletion-impact endpoint, the audit log, and the per-environment breakdown.
  • Delayed Secret Rotation No Longer Records a Refresh That Did Not Happen — markDelayedSecretReauthorizing set refresh_performed=true in the execution metadata before the single re-authorization call actually ran, so a crash or failure before AuthorizeRotationConfigPublish persisted a misleading refresh_performed=true even though no refresh completed. The placeholder now records refresh_performed=false; the flag is set to true only on the successful re-authorization path. At-most-once semantics are unchanged — the loop guard relies on refresh_attempted + refresh_generation.

Removals

  • ProjectCAOverride Removed — The project_ca_overrides table and all associated code (model, handlers, service method findProjectCAOverride) have been removed. This feature was never exposed via the API router (routes were not registered) and the table was always empty in production. The renewal orchestrator now resolves CA exclusively from the template. Migration 000188 drops the table.
  • buildTemplateFromCert Removed — The synthetic template fallback for certificates without a template has been removed. Certificates must be assigned a proper template before they can be renewed.

Maintenance

  • Dead PKI UI Removed — Three orphaned, unreachable frontend components were deleted after confirming no capability loss: CAManagementTab (its Initialize CA / HSM / CRL capabilities are superseded by the Internal Certificate Authority section) and CertificateTemplatesTab + CertificateTemplateModal (superseded by the variable-driven Project Templates certificate flow, which covers subject DN and SANs via template variables). The live CertificateTemplateViewModal (used by the project Templates tab) is retained.
  • Migration 000188 Header Comment Corrected — The header comment in 000188_drop_project_ca_overrides.up.sql incorrectly referenced "Migration 000187"; it now correctly reads "000188". No functional change.

Migration Notes

  • Migration 000188_drop_project_ca_overrides drops the project_ca_overrides table. This is a non-destructive change as the table was never populated through the application (API routes were never registered).
  • Certificates without a TemplateID that previously relied on the internal CA fallback will now report "no template assigned" in the rotation dashboard and cannot auto-renew until a template is assigned.

Notes

  • Upgrade Impact — Version 1.0.58 adds two database migrations: 000182_add_challenge_type_to_certificate_templates (adds a challenge_type VARCHAR(20) DEFAULT 'auto' column with a CHECK constraint to certificate_templates) and 000188_drop_project_ca_overrides (drops the always-empty project_ca_overrides table). Both migrations are idempotent and backward-compatible; existing templates default to auto and existing ACME issuance behavior is unchanged. No configuration schema changes.
  • Terraform Provider — Existing mazevault_ca_account configurations continue to work; the new ACME attributes are optional. mazevault_certificate_template gains an optional, defaulted challenge_type attribute. The internal CA is intentionally not managed via Terraform — it is managed in the MazeVault UI.

Version 1.0.57

Release Date: 2026-07-07

Bug Fixes

  • Rotation Sync Steps (pre/post_rotation_sync) Now Use Managed Identity Instead of Stale Config Credentials — When the rotation orchestrator's pre_rotation_sync or post_rotation_sync steps resolved Azure credentials via getAzureCredentialFromConfig, the function auto-detected client_secret fields from the encrypted integration config blob even when the integration was configured with background_auth_method=managed_identity. This caused AADSTS7000215 (invalid client secret) errors during rotation sync steps while the identical Key Vault connection succeeded during non-rotation sync paths. The credential helper now prefers system managed identity when auth_method is empty and the system has a configured MI, preventing stale SSO app credentials from being picked up. Additionally, the gateway delegation layer no longer blocks delegation solely because managed identity is present, and delegated sync steps now carry a secret_metadata snapshot so gateway-executed sync steps can resolve the target secret key without requiring a direct database lookup.
  • Gateway Delegation No Longer Blocked by Managed Identity Presence — canDelegateEnvironment() previously returned false unconditionally when the credential factory reported managed identity availability, preventing any Azure KV sync delegation to gateways in environments where MI was configured. This gate has been removed; gateway delegation now relies solely on environment matching and gateway availability, allowing MI-authenticated sync operations to be properly delegated.
  • Orchestrator Mode Rotation No Longer Performs Duplicate Key Vault Push — In orchestrator mode (storage_mode=names_only), the mazevault_secret rotation step previously called SecretService.Update which internally pushed the new value to the external vault via PushSecretToEnvironment. Since post_rotation_sync also pushes the same value to the vault, every rotation produced a redundant duplicate write. The step now calls BumpVersion() (local version counter increment only) in orchestrator mode and defers the sole external write to post_rotation_sync.
  • Orchestrator Mode Rotation Compensate No Longer Fails on Empty EncryptedValue — When a rotation saga rolled back the mazevault_secret step in orchestrator mode, Compensate() attempted to decrypt the previous version's EncryptedValue which is always NULL/empty for names_only secrets. This caused spurious "partially_rolled_back" statuses. The compensate path now returns nil immediately for orchestrator-mode secrets — rollback of the external vault value is handled by PostRotationSyncStep.Compensate which has the per-integration pre-overwrite snapshot.
  • Pre-Rotation Sync No Longer Round-Trips Value Back to Key Vault in Orchestrator Mode — When pre_rotation_sync detected a newer value in Azure Key Vault, it called UpdateSecret which in orchestrator mode pushed the same value right back to the vault (redundant network call). In orchestrator mode the step now captures the KV value as previous_value context for downstream steps without triggering a local DB write or external push.
  • Agent Discovery Report No Longer Silently Drops Metadata Update — ProcessDiscoveryReport() persisted discovered certificates into the database but never updated the agent's own metadata (last_discovery_at, discovered_certificates_count). After a successful discovery run the agent row remained stale, making it impossible for operators to tell from the dashboard whether an agent had ever performed discovery or how many certificates it found. The transaction now concludes with an atomic count query and agent row update so dashboard stats reflect reality immediately after each report.
  • Agent Version Not Persisted from Health Heartbeat — UpdateHealth() stored the full health payload into the health_status JSONB column and refreshed last_seen, but never extracted the agent_version string into its dedicated column. Agents reporting their version via the heartbeat appeared as version-unknown in the UI. The handler now copies agent_version from the health map into the agent model before saving.
  • Empty Environment Dropdown in Bind Agent Dialog — The "Bind Agent" dialog on the Project Integrations tab fetched environment data from GET /projects/:id/environment-settings (which returns ProjectEnvironmentSetting[] with an environment field) but rendered the list expecting Environment[] objects (with slug and name fields). Because the field names didn't match, every dropdown item appeared blank. The dialog now fetches from GET /organizations/:orgId/environments which returns the correct Environment[] shape, populating the dropdown correctly.

Version 1.0.56

Release Date: 2026-07-01

Improvements

  • Release Pipeline Security Gating Expanded — The development CI pipeline now scans all shipped images, including docs and init-certs, and the release pipeline now scans docs as well and fails the release on CRITICAL/HIGH Trivy findings instead of reporting them passively. This closes a blind spot where auxiliary images could ship with unresolved vulnerabilities even when backend, frontend, and ocsp were clean.
  • Base Image and Local Runtime Alignment — init-certs now builds from the pinned Dockerfile.init-certs path in local docker-compose instead of running an ad-hoc alpine:latest bootstrap command. The init-certs image moved to Alpine 3.21 with a package upgrade during build, and the docs runtime stage now refreshes Alpine packages before packaging nginx. This reduces drift between CI artifacts and local runtime behavior and lowers exposure to known OS-package CVEs.
  • Backend Dependency Refresh for Reachable Integrations — Patched Go versions are now pinned for github.com/Azure/go-ntlmssp, github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream, github.com/aws/aws-sdk-go-v2/service/s3, github.com/quic-go/quic-go, and go.mongodb.org/mongo-driver, covering reachable optional paths such as ADCS NTLM authentication, S3 audit archive transport, QUIC stack usage, and MongoDB reconciliation and rotation code.

Bug Fixes

  • Request ID Tracing Normalized Across Middleware and Logs — RequestIDMiddleware now populates both requestID and the legacy request_id Gin context key while continuing to emit the X-Request-ID response header, and environment-resolution warnings now read the canonical requestID key. This removes a traceability gap where some logs emitted an empty request ID even though a request ID had already been assigned.
  • Orchestrator Mode Guard Uses Exact Route Patterns — The orchestrator ingress guard no longer relies on raw URL suffix and substring heuristics. It now matches registered Gin route patterns via c.FullPath() for secret write routes in both /api/v1/secrets and /api/v2/secrets, while avoiding false positives on nested certificate import flows. Direct secret payloads carrying value remain blocked in orchestrator mode, but legitimate certificate-import endpoints are no longer at risk of accidental interception.
  • Entra Selected-App Sync Now Stays Pending Until Scoped Bindings Exist — When an Entra integration is configured with selected_apps scope but no selected application bindings have been saved yet, a manual sync now returns HTTP 409 Conflict instead of being reported as a generic failure. The integration is recorded with last_sync_status = pending, last_sync_error stays empty, and the hourly scheduler logs a skip until at least one selected application is configured. This makes the API and operational status truthful during staged onboarding.
  • Sync Rule Status and Audit Surfaces Now Preserve Execution Provenance — Sync executions now persist a canonical execution record and propagate the resulting execution_id, trigger_source, trigger secret/environment, and aggregate outcome counts into rule status and sync-log metadata. Bidirectional sync also suppresses the immediate echo-push of secrets that were just pulled or left unchanged by the pull stage, reducing pointless external churn while keeping sync_rules.last_sync_status and project_integrations.last_sync_* aligned with the final outcome.
  • Rotation Step 0 (connection_validation) Now Respects Integration background_auth_method — When the rotation orchestrator executes locally (non-delegated path), the TestConnection() method for Azure Key Vault integrations now calls prepareAzureIntegrationRuntimeConfig() before attempting the connection. Previously, stale client_secret credentials in the encrypted integration config would override the configured background_auth_method=managed_identity, causing ClientSecretCredential authentication failed errors during rotation while sync (which already used the correct preparation path) continued to succeed. The fix ensures auth method resolution is consistent across sync and rotation execution paths.

Notes

  • Upgrade Impact — Version 1.0.56 contains no database migrations and no configuration schema changes. Existing API contracts remain stable; the main operational change is stricter release gating, so CI and release jobs can now fail if docs or init-certs images contain unresolved CRITICAL/HIGH findings.
  • Local Development Impact — The first docker compose up --build after upgrading rebuilds the dedicated client-init-certs image instead of downloading a generic Alpine base and running inline package installation. Build time may increase slightly, but local behavior now matches the shipped image path more closely.

Version 1.0.55

Release Date: 2026-06-29

New Features

  • Secret Rotation Targets — Full CRUD management for secret rotation targets is now available via API, frontend, Go SDK, and Terraform. Targets define the deployment destinations (Kubernetes secrets, database password rotation, agent sync, DevOps pipeline variables, cloud vaults) that MazeVault pushes updated credentials to immediately after a rotation completes. The new SecretRotationTargetList component in the Secrets tab lets operators add, edit, and remove targets without leaving the project view. The Terraform resource mazevault_secret_rotation_target enables fully declarative management; the config_json argument accepts a jsonencode()-encoded JSON object describing the target-specific configuration.
  • Internal CA Configuration Update + AIA Backfill — Two new API endpoints allow updating mutable fields on an internal CA account without recreating it: PUT /api/v1/ca/:id and PUT /api/v1/projects/:id/ca/:caId accept ocsp_url, crl_url, and ocsp_responder_cert_id with patch semantics (omitted fields are left unchanged). A companion POST /api/v1/admin/certificates/backfill-aia endpoint retroactively copies the current OCSP and CRL URLs from a CA account into all existing certificates it issued. OCSP URL, CRL Distribution Points, and Issuer Certificate URL extensions are now properly embedded in the x509 template inside InternalCAProvider.SignCSR() for all newly issued certificates.
  • Agent Selection in Rotation Config UI — The SecretPostRotationActionsPanel and SecretRotationTargetEditor now expose a dropdown that allows an operator to bind a specific registered agent to a rotation target. This gives precise control over which on-premise agent executes a deployment step without relying solely on environment-based routing.
  • Certificate Template View Modal — A new CertificateTemplateViewModal component provides a full-detail read-only view of any certificate template (SAN patterns, key usage, extended key usage, validity period, CA account binding) without navigating away from the current page. The modal is accessible from both the organisation-level CertificateTemplatesTab and the project-level TemplatesTab. The view now also surfaces the signature algorithm (falling back to the CA default when unset) and the ACME profile name, and annotates validation-only constraints and external-CA-governed fields so operators understand which settings are enforced by MazeVault versus the issuing CA.
  • Certificate Templates Now Govern Issuance End-to-End — Certificate templates have been promoted from a partially-decorative metadata record into the authoritative governance "cookbook" for certificate issuance across the full lifecycle. Template fields that were previously stored but never applied are now honoured by the signing engine and the CSR generator: max_path_length (with explicit path-length-zero handling for CA certificates), certificate policy OIDs (policy_identifiers / certificate_policies), signature_algorithm, the default common-name template (cn_template), and the subject email. A new Governance tab in CertificateTemplateModal exposes approval requirements, allowed environments, and allowed key sizes, and a dedicated Renewal & Rotation section makes the key-reuse policy independently settable. Template defaults are now normalised on create/update (validity_days → 365, key_size → 256/2048 by algorithm, min/max validity windows) so a template can never persist inert zero values.
  • Project & Config Template View Modals — Two new read-only detail modals, ProjectTemplateViewModal and ConfigTemplateViewModal, bring the same at-a-glance inspection experience already available for certificate templates to project templates (general settings, environment rules, variable schema, protocol configuration) and configuration templates (metadata, definition summary, raw JSON). Both are reachable via a view (eye) action in their respective management tabs.
  • Terraform: Certificate Rotation Config, Entra Rotation Config, and Rotation Resources Data Source — Three new Terraform surfaces complete IaC coverage of the rotation platform: mazevault_certificate_rotation_config manages automatic renewal scheduling and post-rotation action lists for a specific certificate; mazevault_entra_rotation_config manages Entra ID credential rotation settings including days-before-expiry threshold and staged-rotation behaviour; mazevault_rotation_resources is a data source that lists all rotation-managed resources with their current status, filterable by kind and environment_scope.

Environment Identity Reform

  • Environment Identifiers Are Now Stored Verbatim and Matched Exactly — Environment identifiers entered in Organization Settings are the single source of truth. The system now stores them verbatim (case-preserving) and matches them exactly (case-sensitive) everywhere — RBAC, secrets, sync rules, gateway routing, gateway tasks, environment locks, and the frontend. Identifiers that differ only in case (for example developmentA, DeveLopmentB, DeveLopmentC) are treated as distinct environments. The system never silently rewrites, lowercases, trims, or alias-maps what an operator entered.
  • CanonicalizeEnvironment is now validate-only: it rejects empty/whitespace-only, reserved (*, default, all, system, none), too-short/long, and malformed identifiers (allowed shape ^[A-Za-z][A-Za-z0-9_-]{1,31}$), and returns the input unchanged on success.
  • The gateway environment alias map (pro→production, npr→nonproduction, etc.) has been removed from both backend and frontend. SupportsEnvironment(), GetEnvironmentMode(), gateway routing, health-monitor failover, and the frontend environments helper now compare identifiers exactly. The frontend environment helper no longer applies toLowerCase()/token normalization, and all environment selectors source their options exclusively from the organization environment list.
  • Impact: existing customer environment data is uniformly lowercase, so exact case-matching is a no-op for current records — zero RBAC/secret/sync regression. Mixed-case identifiers are supported going forward only.

Improvements

  • Rotation Pre-Execution Probe Completeness (Gateway, Agent, and Entra Runtime) — The rotation orchestrator now verifies every required execution target is reachable before the first mutating step, and returns a structured HTTP 412 error when a rotation cannot safely start. Three new error codes — rotation_blocked_no_gateway, rotation_blocked_agent_offline, and rotation_blocked_runtime_unavailable — are surfaced instead of an opaque 500/409. The probe is now certificate-aware (it resolves CertificateRotationConfig targets correctly), collects all required agent IDs across every agent-bearing workflow step, and is also enforced inside executeCertificateWorkflow. Entra credential rotation runs the same runtime probe in RotateCredentialWithRequest before the lifecycle transitions to rotating, so a blocked runtime never leaves a credential half-rotated. A credential or certificate change that cannot be deployed now fails fast and visibly rather than starting and stalling.
  • Gateway Delegation for Azure Key Vault Sync — Azure Key Vault push and pull sync operations are now delegated to the appropriate gateway when the backend detects that the direct network path to the Key Vault endpoint is not available. Gateway tasks carry the integration configuration and perform the actual Azure SDK calls; the primary backend records only the resulting sync state. The sync rule scheduler start has been deferred to after gateway delegation is fully initialized, preventing transient DNS resolution errors during backend startup.
  • Rotation Config — Target Environment Auto-Fill — When GetConfigBySecretID creates a new rotation configuration for a secret that does not yet have one, target_environment is now automatically populated from the secret's own environment. This eliminates a manual follow-up update step that was previously required before the first rotation could be triggered.

Bug Fixes

  • Gateway Environment Matching Reworked to Verbatim/Exact Identity — Earlier in this release cycle, gateway environment matching relied on a case-insensitive alias map (pro→production, npr→nonproduction, etc.) that could silently map unrelated identifiers together and mutate operator-entered values. This approach has been replaced by the verbatim/exact environment-identity model (see Environment Identity Reform above): SupportsEnvironment() and GetEnvironmentMode() now compare exactly, the alias map is removed, and the previously planned alias-normalization migration 000176 has been neutralized to a no-op (it was never shipped to customers). Forward migrations 000178/000179 remove the lowercase-enforcing CHECK constraints and case-insensitive unique indexes so case-distinct identifiers are accepted.
  • AIA Extensions Not Embedded in Issued Certificates — InternalCAProvider.SignCSR() read ocsp_url and crl_url from the CA record but never wrote them into the x509 certificate template, so every certificate issued by an internal CA had empty Authority Information Access and CRL Distribution Points extensions. Both extensions are now correctly embedded for all new certificates. Existing certificates can be retroactively updated via the new backfill endpoint.
  • Device Fingerprint — Concurrent Insert Race Condition — The anomaly detection service could produce duplicate-key constraint violations when two concurrent authentication requests from the same previously unseen device tried to register its fingerprint simultaneously. The insert path now uses an upsert pattern with an ON CONFLICT DO NOTHING guard to handle concurrent registrations gracefully.
  • Sync Rule Scheduler — DNS Errors at Backend Startup — On backends configured with gateway delegation, the sync rule scheduler's first scheduled execution could fail with DNS resolution errors because it started before the gateway delegation subsystem had completed initialisation. The scheduler start is now deferred until the delegation layer is ready.
  • Terraform Organization Resource — Delete Silently Swallowed Errors — The Delete method in the Terraform mazevault_organization resource returned success even when the backend responded with a non-2xx status code. Backend errors are now surfaced as Terraform diagnostics so terraform destroy fails visibly instead of leaving the resource in an inconsistent state.
  • Certificate Policy OIDs Silently Dropped from Issued Certificates (Go 1.24+) — InternalCAProvider.SignCSR() populated only the deprecated x509.Certificate.PolicyIdentifiers field. Go 1.24+ x509.CreateCertificate emits the Certificate Policies extension from the new Policies ([]x509.OID) field instead, so template-declared policy OIDs were silently omitted from every issued certificate. SignCSR now populates both fields — the modern Policies slice (for current encoders) and the legacy PolicyIdentifiers slice (for older parsers) — so policy OIDs are reliably embedded.
  • Certificate Template Signing & Subject Fields Were Inert — max_path_length, signature_algorithm, certificate policy OIDs, the default common-name template, and the subject email were persisted on certificate templates but never reached the issued certificate or generated CSR. All of these template fields are now applied during signing and CSR generation, closing the gap between what a template declares and what a certificate actually contains.
  • External CA Template Import Persisted Zero validity_days / key_size — Templates imported from external CA providers (ADCS, SmallStep) were stored with validity_days = 0 and key_size = 0, which then surfaced as invalid defaults in the template editor. Imported templates now carry sane defaults (key_size = 2048, valid_days = 365), and the service layer normalises any remaining zero values on create/update as a defence-in-depth backstop.

Database Migrations

  • 000176_normalize_gateway_environments — Neutralized to a no-op (SELECT 1). This migration originally normalized legacy environment aliases to canonical slugs, but the environment identity reform reversed that direction (identifiers are now stored verbatim). Because the migration was never released to any customer, both its .up.sql and .down.sql are now documented no-ops, leaving customer gateway data untouched.
  • 000177_certificate_templates_signature_algorithm — Adds the signature_algorithm VARCHAR(50) column to certificate_templates (idempotent ADD COLUMN IF NOT EXISTS) so templates can pin the issuance signature algorithm. The matching .down.sql drops the column. ExpectedColumns["certificate_templates"] in database_health.go was updated accordingly.
  • 000178_relax_canonical_environment_check — Drops the 11 chk_*_canonical CHECK constraints (added by migration 000125) that required column = LOWER(column) on every environment-bearing column. These constraints would reject case-distinct identifiers, contradicting the verbatim contract; application-side validation (CanonicalizeEnvironment) continues to enforce shape, length, and reserved-word rules. The down migration re-adds the constraints. Idempotent (DROP CONSTRAINT IF EXISTS).
  • 000179_case_sensitive_environment_indexes — Replaces the case-insensitive LOWER() unique guardrails from migration 000122 with an exact, case-sensitive unique index uniq_environments_org_slug (organization_id, slug) and restores plain raw-column lookup indexes for every environment-bearing column. Existing per-project uniqueness (idx_project_env) is already exact. The down migration restores the LOWER() indexes. Idempotent.

Version 1.0.54

Release Date: 2026-06-27

Bug Fixes

  • Entra Credential Watchdog — Stuck Detection Permanently Bypassed by Hourly Sync — The PostgreSQL trigger trg_entra_creds_updated refreshes updated_at = NOW() on every UPDATE to entra_credentials, including the non-state field writes performed by the hourly reconciliation sync (syncedEntraCredentialUpdates). This meant updated_at < now() - 30min was never true for any credential that went through a sync cycle, preventing the watchdog from ever firing. A new rotation_started_at TIMESTAMPTZ column (migration 000175) is set when lifecycle_state transitions to rotating and cleared on rollback or watchdog recovery. The watchdog query now uses rotation_started_at IS NULL OR rotation_started_at < cutoff; IS NULL covers credentials already stuck before the migration was applied.
  • Entra Expiry Monitor — Preload("") GORM Error on Every Hourly Check — checkExpiringCredentials called .Preload("") (empty string) on the GORM query. GORM v2 rejects this with unsupported relations for schema EntraCredential, logging an error every hour and preventing the expiring_soon lifecycle state from being set on credentials that genuinely are expiring. The invalid .Preload("") call has been removed; behavior is otherwise unchanged.
  • Sync Rule Pull — secret_versions_created_by_fkey FK Violation for System Actors — SyncRuleService.executePull calls SecretService.Update with req.UserID = uuid.Nil (no authenticated user). Update unconditionally stored &req.UserID as created_by in the new SecretVersion row; GORM serializes *uuid.Nil as the string '00000000-...' rather than SQL NULL, which violates the non-null FK referencing the users table. The fix adds a nil-guard: created_by is set to NULL when the caller supplies a zero-value UUID, matching the nullable column contract.
  • Azure KV Integration — Synced Secrets Displayed as "Stored Locally" — syncAzureKeyVaultLocal created and updated secrets in the secrets table but never inserted a SecretIntegrationLink row with link_purpose = 'sync_source'. GetSecretSyncStatus therefore returned local_only for every KV-synced secret and the UI showed "Stored Locally" instead of "Synced from KV". The fix inlines the same upsert logic used by SyncRuleService.upsertPullLink directly in IntegrationService using s.db and clause.OnConflict, avoiding a cross-service dependency.

Improvements

  • Role Permissions — agent:admin Added to secret_manager and certificate_manager Roles — Agent management operations (agent:admin) have been added to the secret_manager and certificate_manager system roles (migration 000174). Operators holding only one of these roles can now register and configure agents without requiring a separate admin role assignment.

Database Migrations

  • 000174_agent_admin_for_managers — Grants agent:admin permission to the secret_manager and certificate_manager built-in roles.
  • 000175_entra_credentials_rotation_started_at — Adds rotation_started_at TIMESTAMPTZ to entra_credentials. Includes partial index idx_entra_creds_rotation_started over (rotation_started_at) for the rotating and pending_verification lifecycle states.

Version 1.0.53

Release Date: 2026-06-26

Bug Fixes

  • OCSP — Certificate Existence Not Verified Before Stapling — The OCSP service responded to stapling requests without first confirming that the referenced serial number exists in the local certificate store. An attacker with a forged or recycled serial could have elicited a valid OCSP response for a non-existent certificate. The service now verifies certificate existence before constructing a response, returning Unknown status for unrecognised serials.
  • Rotation Step — PostgreSQL Grants Vulnerable to SQL Injection — rotation_steps/postgres_db.go constructed GRANT/REVOKE statements by string-concatenating the caller-supplied role name and privilege list without quoting. Database role names are now double-quoted using pgx-style identifier escaping, closing the injection path.
  • SSH Keys — ExportPrivateKey Missing Authorization Check — ExportPrivateKey did not verify that the requesting user had ownership or admin rights on the SSH key before returning the encrypted private material. The handler now enforces the same project-membership and ownership check applied to all other SSH key mutation endpoints.
  • SSH Rotation — Incorrect API Response Field Mapping for Key Pairs — The SSH rotation response handler mis-mapped key-pair fields, causing the returned public_key and key_id to be swapped or empty after a successful rotation. The field mapping has been corrected.
  • Keytab Rotation — Unstructured Logging Bypassing Structured Pipeline — Keytab rotation errors were emitted via the legacy log package, bypassing the structured logging pipeline and losing request-scoped fields. All log calls in keytab_rotation_executor.go are now routed through slog.
  • CA Accounts — ca_authority_id Not Persisted on Update — UpdateCAAccount silently discarded ca_authority_id when updating an existing CA account because the field was absent from the GORM Updates map. The field is now included and covered by a new test.
  • CRL / Revocation Schema Drift — OCSP Returns 500 — The revocation_entries and certificate_revocation_lists tables were created by migration 000005 with legacy column names (reason_code, crl_pem) that diverged from the current GORM model fields (revocation_reason, raw_content). GORM appends deleted_at IS NULL to every query; because deleted_at was absent from the production schema the entire CRL/OCSP subsystem returned SQLSTATE 42703 errors. Migration 000173 adds all missing columns with idempotent ADD COLUMN IF NOT EXISTS guards and backfills values from the legacy columns.

Improvements

  • Rotation Trigger — Request Body Optional — The rotation trigger endpoints now accept requests with an empty or absent body; reason defaults to "manual" when not supplied.
  • CA Internal — HSM OCSP Signing Deferred with Explicit Documentation — OCSP response signing via HSM is explicitly deferred; the code path now carries a clearly marked comment and returns a software-signed response in the meantime, preventing silent fallbacks.
  • Feature Flags — Gateway Mode and License Validation Hardened — features.go tightens the evaluation logic for gateway_mode and license-gated features, eliminating a class of false-positive enabled results when the license payload is partially parsed.
  • SRP — MFA Step Sequencing Documented — The SRP session-verification flow now carries an explicit comment documenting MFA step ordering and the rationale for the current TOTP/backup-code sequence.
  • Secret Naming Policy Wizard — Auth Context Threaded to Token Management — The SecretNamingPolicyWizard now passes the authenticated user context through to token-management calls, closing a potential unauthenticated call path in the wizard's save flow.

Database Migrations

  • 000173_fix_revocation_crl_schema_drift — Adds missing columns to revocation_entries (crl_id, revocation_reason, updated_at, deleted_at) and certificate_revocation_lists (raw_content, updated_at, deleted_at). Backfills from legacy columns reason_code and crl_pem. Fully idempotent.

Version 1.0.52

Release Date: 2026-06-23

Bug Fixes

  • Renewal Policy Schema — key_reuse_enabled and validity_duration Missing from Database — The certificate_renewal_policies table was created by migration 000010 using the column name reuse_private_key; the Go model was later renamed to key_reuse_enabled without a companion migration, causing every write that touched the field to fail at the database layer. Migration 000172 adds key_reuse_enabled BOOLEAN NOT NULL DEFAULT FALSE (with a backfill from the legacy reuse_private_key column) and validity_duration INT NOT NULL DEFAULT 0. The same migration converts notify_emails from JSONB to TEXT using a three-step staging approach (add TEXT column → UPDATE via set-returning function → drop JSONB column → rename), working around the PostgreSQL restriction that forbids subqueries inside ALTER COLUMN TYPE … USING.
  • Renewal Queue — certificates.organization_id Column Does Not Exist — GetRenewalQueue filtered WHERE certificates.organization_id = ? but the certificates table only carries project_id; organization is reachable via certificates.project_id → projects.organization_id. The service now performs a second JOIN (JOIN projects ON projects.id = certificates.project_id) and filters on projects.organization_id.
  • SDK and Terraform Provider — notify_days_before []int Semantic Mismatch — The Go SDK (sdks/go/models.go) and Terraform provider (internal/resources/renewal_policy.go) declared a notify_days_before []int field that has never existed in the backend. The backend stores a plain comma-separated string under notify_emails. Both were rewritten to use notify_emails string, and the two new fields key_reuse_enabled and validity_duration were added to align with the live API contract. Acceptance tests that were blocked with t.Skip due to these gaps are now live.

Database Migrations

  • 000172_fix_certificate_renewal_policy_schema — Adds key_reuse_enabled BOOLEAN NOT NULL DEFAULT FALSE (backfilled from reuse_private_key), validity_duration INT NOT NULL DEFAULT 0, and converts notify_emails JSONB → TEXT to certificate_renewal_policies.

Version 1.0.51

Release Date: 2026-06-22

New Features

  • Acknowledge Rotation Failure — Operators can now permanently dismiss a reviewed rotation failure from the Rotation Problems dashboard without deleting the underlying execution record. A new POST /api/v1/dashboard/rotation/failures/{id}/acknowledge endpoint (RBAC: dashboard:write) writes acknowledged_at and acknowledged_by to the execution row. The dashboard query filters acknowledged rows, keeping the view focused on items that still require attention. The UI exposes a "Mark as reviewed" action from the failure detail panel.
  • Entra Credential Stuck Watchdog — A new background watchdog (RecoverStuckEntraCredentials) runs every 5 minutes and resets Entra credentials stuck in rotating or pending_verification lifecycle state for more than 30 minutes. This prevents indefinite stalls caused by backend restarts or goroutine cancellations mid-rotation. Each recovery writes stuck_watchdog_recovered_at to the credential row for auditability.
  • Entra Credential Status — pending_verification — The entra_credentials_status_check constraint was extended to allow the pending_verification value written by RotateCredentialWithRequest after every rotation when downstream propagation targets are configured. Without this, any rotation with propagation would produce a constraint violation and leave the credential in an inconsistent state.
  • CA Account — Assigned Certificates Count — The CA account list and detail responses now include assigned_certificates_count, the number of active certificates issued via certificate templates bound to that CA account. This gives operators an immediate signal of a CA account's active footprint without an additional query.
  • Terraform Provider — User, User Role, Keytab, Project Settings, Shared Secret — Five new Terraform resources complete the provider's coverage of the MazeVault user-management and project-configuration surfaces: mazevault_user (create/read/update/delete of platform users), mazevault_user_role (assign roles to users within an organization), mazevault_keytab (manage Kerberos keytab files attached to a project), mazevault_project_settings (project-scoped configuration knobs), and mazevault_shared_secret (secrets shared across multiple projects).

Improvements

  • Rotation Problems — Acknowledged Filter — The Rotation Problems page and the platform summary service exclude acknowledged executions from failure counts and lists by default, using the partial index idx_rotation_executions_unacknowledged introduced in migration 000170.

Database Migrations

  • 000169_entra_credentials_status_pending_verification — Extends the entra_credentials_status_check constraint to include pending_verification.
  • 000170_rotation_execution_acknowledgement — Adds acknowledged_at TIMESTAMPTZ and acknowledged_by UUID REFERENCES users to rotation_executions. Includes partial index idx_rotation_executions_unacknowledged covering only un-acknowledged rows.
  • 000171_entra_credential_stuck_watchdog — Adds stuck_watchdog_recovered_at TIMESTAMPTZ to entra_credentials. Adds partial index idx_entra_creds_stuck_watchdog over (lifecycle_state, updated_at) for the two in-flight states.

Version 1.0.50

Release Date: 2026-06-21

New Features

  • Rotation Config Templates — A new organization-scoped catalog allows operators to record reusable rotation settings (interval, lead days, retention window, max retries, timeout). Managed via five API endpoints under GET|POST /api/v1/rotation/templates and GET|PUT|DELETE /api/v1/rotation/templates/{id} with RBAC (rotation:read / rotation:write). A dedicated Terraform resource mazevault_rotation_template enables IaC-driven catalog management. Templates are not automatically applied to rotation configurations. The frontend exposes a full CRUD management page at /rotation/templates. Migration 000168 adds the rotation_config_templates table and a template_id foreign key on rotation_configs.
  • Sync Rule — Bidirectional Mode and Conflict Strategy — The SyncRuleModal now exposes configurable sync direction (pull / push / bidirectional) and sync mode (incremental / full_sync) fields. Both attributes are persisted server-side and documented in the new Terraform resource mazevault_sync_rule. Pull-sync operations now write last_pulled_at, link_purpose, and external_secret_path into secret_integration_links (migration 000167), enabling the synced_from_external status badge on secrets that were pulled from an external provider.
  • Dry-Run for Certificate Rotation — The Certificate Rotation tab now includes a "Dry Run" button alongside the existing "Trigger Rotation Now" action. Dry-run executes the full rotation workflow validation — including post-rotation action preflight and gateway probing — without committing any changes. The dry_run flag is propagated end-to-end through the API and all rotation steps.
  • Post-Rotation Action — Gateway Pinning and Environment Override — PostRotationAction now accepts an optional gateway_id field that pins the action to a specific gateway, bypassing environment-based routing. A companion target_environment field overrides the environment context used when resolving the target. Both fields are available in the Go SDK (PostRotationActionWF) and in the Terraform mazevault_rotation_workflow resource.
  • Terraform Provider — Sync Rules and Rotation Templates — Two new Terraform resources cover the full create/read/update/delete lifecycle: mazevault_sync_rule (sync direction, conflict strategy, key transform, path prefix) and mazevault_rotation_template (interval, lead days, retention, retry policy). Both are registered in the provider and documented in docs/resources/.
  • Rotation Problems — Failed Executions Detail View — The Rotation Problems page now renders a detailed failed-execution table with per-resource drill-down, a pie-chart distribution by resource kind, and a missing-secrets breakdown. Navigation from the sidebar highlights the section with a warning icon.

Improvements

  • Distributed Sync Semaphore via Redis — SyncRuleService now uses a Redis-backed distributed lock (SET NX PX) through a new TryAcquireLock / ReleaseLock pair on CacheService. When Redis is unavailable the service falls back to an in-process sync.Map lock. This prevents duplicate concurrent syncs for the same rule in multi-replica deployments.
  • Dry-Run Guard in All Rotation Steps — All seven built-in rotation steps (password generator, Azure Key Vault, Spring Actuator, agent command, shell script, IIS recycle, Kubernetes secret) now check the dry_run flag before making any mutating external call and return a preview result instead.
  • Connection Validation Scope Extended — The connection_validation rotation step was extended to cover non-database provider types (Azure Key Vault, Kubernetes, LDAP, GitHub, GitLab, generic webhook), ensuring pre-execution connectivity checks are not silently skipped for secrets backed by those integrations.
  • names_only Secret Rotation Guard — The rotation orchestrator now rejects rotation requests for secrets with storage_mode = names_only with a clear error message, preventing misleading execution records for secrets whose value lifecycle is managed entirely by the external provider.
  • Secret Sync Status — synced_from_external Indicator — GetSecretSyncStatus now returns synced_from_external when a secret has been successfully pulled from an external provider. The Secrets tab surfaces this as an informational "Synced from KV" badge.
  • Compliance Report — Deployment and Sync Coverage Stats — PasswordPolicyStats is extended with secrets_with_deployment_target, secrets_with_external_sync, and secrets_with_template counters. The compliance report service gathers these in a single additional DB pass.
  • Gateway Task Service — Direct Gateway ID Pinning — GatewayTaskService.CreateTask now accepts an optional GatewayID that, when set, skips the routing-service lookup entirely and dispatches the task directly to the named gateway. This supports pinned post-rotation actions without routing ambiguity.
  • Code Readability — Internal service and model struct fields were reformatted for consistency. resolveRotationResourceIncidentTarget parameters were simplified, removing redundant intermediate variables.

Bug Fixes

  • Dashboard Dismiss — TypeScript Type Narrowing — The dismiss handler for actionable rotation items now correctly narrows item.status from 'overdue' | 'failed' | 'running' to the server-accepted 'overdue' | 'failed' union before sending the dismiss request, fixing a TypeScript compile error introduced when 'running' was added to the item status type.
  • Rotation Template Routes Missing After Refactor — The five /api/v1/rotation/templates route registrations were inadvertently removed during a code-formatting pass. All five routes have been restored with RBAC middleware (rotation:read / rotation:write).
  • Swagger Path Drift for Rotation Templates — Swagger @Router annotations in handlers_rotation_templates.go incorrectly used /rotation-templates instead of the actual gin route path /rotation/templates. All five annotations were corrected and the OpenAPI spec was regenerated.
  • Audit Logging Missing on Secret Link Operations — CreateSecretLink and DeleteSecretLink handlers now emit SECRET_LINK_CREATED and SECRET_LINK_DELETED audit log entries with the correct resource type, ID, and caller IP.

Database Migrations

  • 000167_secret_integration_links_pull_tracking — Adds last_pulled_at TIMESTAMPTZ, link_purpose VARCHAR(64), and external_secret_path VARCHAR(512) columns to secret_integration_links. Includes a unique constraint uq_sil_secret_integration and a partial index for active links.
  • 000168_rotation_config_templates — Adds the rotation_config_templates table with full CRUD columns (name, description, org scope, default flag, rotation interval, lead days, retention days, retry policy, timeout). Adds template_id UUID REFERENCES rotation_config_templates to rotation_configs.

Version 1.0.49

Release Date: 2026-06-20

New Features

  • Domain Control Validation (DCV) for External CA Orders — External CA providers (DigiCert CertCentral v2, SSLMarket) that require domain ownership proof before issuing a certificate now have full DCV lifecycle support. A new certificate_dcv_tokens table (migration 000166) stores per-domain challenge tokens. The backend automatically fetches tokens after order submission, transitions the renewal queue item to pending_dcv, and polls every 5 minutes. The frontend DCVStatusPanel component shows per-domain challenge details (DNS TXT record name/value, HTTP file path/content, or approver email) with copy-to-clipboard buttons, a progress bar, and a manual Retry Verification action. New API endpoints GET /api/v1/renewal-queue/{id}/dcv and POST /api/v1/renewal-queue/{id}/dcv/retry expose the DCV state with RBAC (certificate:read / certificate:write).
  • ADCS Pending-Approval — Non-Blocking DCOM Submission — The ADCS DCOM method no longer blocks the HTTP handler goroutine with a 5 × 5 s retry loop when the CA manager has not yet approved a request (ADCS status 5). It now returns ErrADCSPendingApproval immediately; the existing ADCSApprovalPoller background job handles asynchronous retrieval once the request is approved.
  • Certificate Request — Advanced Subject DN Fields — The Request Certificate modal now exposes optional Subject DN fields: Organisation (O), Organisational Unit (OU), Locality (L), State/Province (ST), and Country (C). Populated values are sent as a structured requested_subject object in the CSR request payload.
  • Certificate Request — Key Algorithm Override — For MazeVault-generated key pairs, operators can now override the key algorithm (RSA, ECDSA, Ed25519) and key size directly in the Request Certificate modal. The selection is sent as requested_key_algorithm and requested_key_size in the CSR payload.
  • Actionable Rotations Dashboard Panel — The Overview dashboard now includes a dedicated "Rotation operations" panel that surfaces overdue and failed rotation resources requiring immediate attention. Each item shows kind, project, and status badge, and supports a one-click Dismiss flow with audit note. Up to five items are shown, prioritized by age (overdue) and recency (failed).
  • Dashboard Expiring Certificates — 30-Day Filter — The "Direct actions" section of the Overview dashboard now filters expiring certificates to the next 30 days, making the list immediately actionable instead of showing all future expiries. The section is titled "Expiring certificates (next 30 days)". The GET /api/v1/dashboard/certificates/expiring endpoint accepts a new optional days query parameter (1–3650) to scope results; omitting it returns all future-expiring certificates (backward-compatible).
  • Entra Credential Staged Rotation and Dry-Run — Entra credential rotation now supports staged rotation (staged_rotation_enabled) with a configurable soak window (soak_window_hours). Both fields are persisted in the rotation resource metadata and round-trip correctly through the rotation config GET/PUT cycle. A dry-run path validates the rotation workflow against the live Entra tenant without applying any changes.
  • Entra Integration — Instance ID and Protocol Versioning — Entra integrations now carry an instance identifier and a protocol version field, enabling more precise targeting and future compatibility in multi-tenant and delegated execution scenarios.
  • Rotation Scheduler — Stuck-Item Recovery — The rotation scheduler now detects resources stuck in a running or approval-pending state beyond a configurable deadline and resets them to a retriable state, preventing silent indefinite stalls. Renewal orchestration also gained explicit coordination logic for resources transitioning through the renewal window.
  • Certificate Rotation — Next Due Date Calculation — Certificate rotation due-date logic is now covered by dedicated unit tests. The next due date is computed from effective lead-day policy across certificate, template, and CA-account layers, ensuring consistent renewal window behavior.

Improvements

  • Dashboard Data Fetching Resilience — Dashboard data fetching now uses Promise.allSettled instead of Promise.all. Individual widget failures (network error, 401, 404) no longer discard data from all other widgets. Each failure is logged via the structured logger for observability.
  • RBAC Permissions for Rotation Endpoints — Rotation resource read and write permissions are now consistently applied across all new dashboard and rotation endpoints, including archived-resource filtering in service-layer queries.
  • Dashboard Service Archived Resource Exclusion — Platform rotation summary and actionable rotation queries now explicitly exclude archived resources, preventing historical data from inflating overdue or failed counts.

Bug Fixes

  • Entra Rotation Config Edit — Disabled State Regression — Editing an existing Entra credential rotation configuration no longer shows all fields as disabled for non-primary credentials. The GET response now returns rotation_enabled from the stored user preference (metadata.rotation_enabled) instead of from the scheduler gate (resource.Enabled = RotationEnabled && IsPrimary), which was always false for non-primary credentials.
  • Entra Rotation Config — Staged Fields Not Restored on Edit — staged_rotation_enabled and soak_window_hours are now included in the rotation resource projector metadata and are therefore correctly restored when opening an existing rotation configuration for editing.
  • Entra Scope Binding — Tenant ID Not Required — Creating an Entra scope binding no longer requires tenant_id when the integration uses managed identity authentication. The backend validation and the frontend wizard were updated to treat tenant_id as optional for managed identity flows.

Infrastructure

  • Step-CA Network Policy — Added network policy rule to allow inbound traffic to Step-CA on port 9000 from within the cluster, unblocking staging ACME issuance flows that route through the internal Step-CA provisioner.

Version 1.0.48

Release Date: 2026-06-16

New Features

  • PFX Compatibility Profile Options — Certificate PFX export now supports selecting a compatibility profile (legacy / modern) that controls the encryption and MAC algorithms used in the PKCS#12 container. legacy targets OpenSSL 1.x, Java KeyStore tooling, and older Windows endpoints; modern targets OpenSSL 3.x, .NET 6+, and current Go runtimes.

Improvements

  • Log Redaction — Original Request URL Preserved — The log redaction middleware now preserves the original RequestURI on the Gin context before any path rewriting or normalization occurs. This ensures audit and structured log entries reflect the URL the client actually sent, not a post-processing artifact. A regression test covers the URL-preservation contract.
  • Binary Download — ArrayBuffer and Delayed URL Revocation — Certificate and agent binary downloads now use arraybuffer response type and delay object URL revocation until the download has been reliably delivered to the browser, eliminating a race condition on slow connections where the revocation fired before the download completed.

Dependency Updates

  • form-data and hasown — Updated form-data and hasown to their latest patch releases, resolving minor compatibility warnings with current Node.js runtimes.

Version 1.0.47

Release Date: 2026-06-14

New Features

  • Shared Entra Rollout Actions and Readiness Checks — Entra credential rotation now supports ordered rollout actions through the shared rotation platform, including Azure Key Vault, Kubernetes Secrets, agent-managed runtime files, Spring refresh/webhook delivery, and IIS app pool recycle on Windows targets. Dry-run and preflight checks validate the same runtime paths before production execution.
  • Per-Certificate Renewal Key Policy — Certificate operators can now choose per certificate whether renewal regenerates a new private key or reuses the existing key material where the provider and custody path allow it. Target processing order is now preserved explicitly in the renewal workflow.
  • Stronger Entra Integration Authentication Validation — The integration wizard and backend now validate interactive and background Entra authentication settings together, preventing save-time acceptance of configurations that cannot support background sync, dry-run, or post-rotation execution.

Improvements

  • Unified Rotation Readiness in the Project Hub — The Project Rotations view now shows inline readiness and preflight state for Entra credentials, certificate renewal resources, and certificate deployment resources, including clearer manual review required surfacing for rollout targets that cannot be probed automatically.
  • Role and Contract Alignment for Rotation Operations — RBAC migrations and project-surface permission checks were aligned with the expanded rotation and configuration surfaces, reducing cases where operators could open a screen without the permissions needed to complete the workflow.
  • Swagger and Operational Signal Refresh — Generated Swagger output now reflects the updated severity vocabulary and improved host handling, while certificate and database health checks expose stronger diagnostics for rotation readiness and export paths.
  • Secret Rotation API Contract Refresh — The secret rotation API and generated Swagger now document the explicit create/edit/delete lifecycle more accurately, including persisted post-rotation actions and the clean reset path after a secret rotation is removed.

Bug Fixes

  • Entra Lifecycle Cleanup Consistency — Deleting an Entra integration or a locally managed app registration now removes associated Entra credential rotation resources in the same lifecycle operation. Upgrade migration 000155 also removes historical orphan rows, and repository reads defensively hide stale entra_credential entries that predate the cleanup.
  • Certificate Rotation Defaults on Import — When an imported certificate is eligible for managed renewal, MazeVault now prepares the rotation configuration without silently enabling it. Initial lead days are resolved from the effective certificate, template, and CA-account policy instead of being masked by the model default.
  • Rollout Validation and Export Robustness — Validation for agent_id versus direct agent_url rollout steps is stricter, and certificate export flows now return clearer PFX and private-key failures instead of partially masked errors.
  • Secret Rotation Delete and Recreate Semantics — Deleting a secret rotation now removes the rotation config, canonical secret rotation resource, and linked rotation integrations without deleting the secret itself. After deletion, secret status surfaces show Rotation not configured, and reopening the rotation modal stays in create mode until the operator explicitly saves a new configuration.

Version 1.0.46

Release Date: 2026-06-09

New Features

  • External CA Order Poller Baseline — MazeVault added background polling for externally issued certificate orders, allowing asynchronous CA workflows to complete and update the certificate lifecycle without manual tracking.

Notes

  • Release Note Clarification — The Entra rotation rollout, certificate rotation UI unification, and related readiness and preflight work that had previously been drafted for v1.0.46 landed after the tag and are therefore documented under v1.0.47.

Version 1.0.45

Release Date: 2026-06-09

New Features

  • Rotation Resource Registry and Platform Ownership Expansion — The rotation platform is expanding from certificate-only orchestration into a registry-backed resource model. New control-plane services introduce resource kinds, target registries, and shared ownership for rotation scheduling, making future secret, certificate, and Entra rotation flows converge on the same platform primitives.
  • Agent Discovery Policy Bundles and Persistent Config Indexing — Agents now receive backend-driven discovery policy bundles built from active configuration templates. Filesystem discovery persists a local metadata index and reuses cached config and certificate findings across runs, reducing repeated scans while keeping policy-version changes authoritative.
  • Configuration Discovery Submission — Agents can now submit configuration discovery findings back to the backend, including classification and rewrite-plan metadata, using the existing discovery result model.
  • Certificate Template Code Deduplication and Conflict Resolution — Organization certificate template management now deduplicates template codes and blocks ambiguous conflicts. Upgrade migrations normalize existing duplicates so template routing and issuance remain deterministic.
  • System Certificate Classification — Certificates now carry an is_system flag for internal mTLS and identity-management material. MazeVault can retain these certificates for platform use while keeping them separate from normal operator-facing inventory.
  • Integration Groups — Projects now support named integration groups that map logical groupings to integration targets with structured JSONB configuration. Groups can be created, listed, updated, and deleted via the new /api/v1/projects/{id}/integration-groups and /api/v1/integration-groups/{id} API endpoints (migration 000152).
  • Real Slack Incident Notifications — Incident management now dispatches real-time Slack notifications via a configured Incoming Webhook. When an incident is raised on a project with a Slack integration, the platform posts the incident type and resource name to the configured channel.
  • GCP Secret Manager and Kubernetes Connectivity Tests — Integration health checks now validate live GCP Secret Manager API access (listing secrets in a given project) and Kubernetes API server reachability using in-cluster or kubeconfig credentials, in addition to the existing provider tests.
  • HSM Key Operations — HSM providers (PKCS#11, AWS CloudHSM, GCP Cloud HSM, Azure Managed HSM) now expose full key lifecycle operations: GetPublicKey (reconstructs RSA/EC PKIX DER from hardware), DeleteKey (destroys all key objects on the device), ListKeys (enumerates all managed key handles), and GetKeyInfo (returns key type, size, and extractable flag).
  • Vault PKI Full Lifecycle — HashiCorp Vault CA provider now implements the complete CA interface: RenewCertificate, GetCertificateStatus (checks revocation timestamp), ListIssuedCertificates (enumerates via PKI list endpoint), GetOrderStatus, and CancelOrder (revokes the underlying certificate).
  • ADCS Deferred Certificate Retrieval — The ADCS (WCCE) provider now fetches certificates that were issued asynchronously by the CA via a SOAP Renew request. Previously pending certificates are retrieved and returned in PEM format once the CA completes issuance.
  • Venafi VaaS Full Lifecycle — Venafi CA provider now implements RenewCertificate, GetOrderStatus (retrieves pick-up status from VaaS), and CancelOrder.
  • DigiCert Rate Limit Reporting — The DigiCert provider now queries live rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) and surfaces them through the GetRateLimits interface.
  • Config Injection with Live Secret Values — Configuration versioning now resolves ${VAULT:<placeholder>} tokens against live MazeVault secret values at snapshot time, so injected config files carry real credentials without storing them in the config version history.

Improvements

  • Project-Aware Resource Visibility Policies — Azure discovery endpoints now apply resource_visibility_policy filtering in the context of the selected project. This keeps Integration Wizard choices aligned with project scope for non-admin operators instead of exposing subscription-wide results.
  • Configuration Management Auditability — Configuration management flows now record audit-aware rewrite-plan activity so operational changes are traceable alongside the rest of the platform's lifecycle events.
  • Generated CSR and Key Custody Metadata — Certificate request flows now carry generated-CSR and private-key custody metadata forward, improving visibility of whether MazeVault owns, reuses, or must preserve external key material in subsequent lifecycle actions.
  • Delegated Agent Secret Sync Step — Rotation and execution pipelines now have an explicit agent-side secret synchronization step, improving parity between platform orchestration and what managed agents actually apply downstream.
  • Certificate Owner-Surface Navigation — Certificate links from the main dashboard, project rotations, and the certificate inventory now resolve directly to the owning surface. Project-owned certificates open their project detail view, while unassigned inventory opens the Discovered Certificates owner surface via managed_certificate_id, replacing the legacy duplicate /certificates flow.
  • Secret Dashboard Data Consistency — The Secrets Overview tab now uses the shared dashboard API, restoring missing-secret drill-ins and keeping overview counts and decrypted names aligned with the main dashboard data layer.
  • Discovered Certificate Lookup Filter — Discovery APIs and generated Swagger documentation now support managed_certificate_id filtering, enabling targeted deep links from certificate overview tables to the matching discovered certificate record.

Bug Fixes

  • Rotation Scheduling Ownership Cutover — Renewal scheduling is moving to the shared rotation platform owner model so follow-up workflows operate on the current certificate resource instead of stale pre-renewal anchors.
  • Discovery Project Context Consistency — Integration discovery helpers now consistently pass project context, avoiding empty or over-broad Azure discovery responses when resource visibility filtering is enabled.
  • Detect Drift Authentication — POST /api/v1/secrets/drift/detect now executes inside the authenticated secrets route group, fixing 401 failures caused by missing auth context on drift-detection requests.
  • Dashboard Drill-In Regressions — Fixed stale certificate links that still pointed to the removed global certificates page and restored missing-secret links in the Secrets Dashboard overview.

Version 1.0.44

Release Date: 2026-05-25

New Features

  • Certificate Rotation Post-Actions — Certificate rotation now supports explicit post-rotation action chains, including webhooks and other registered post-processing steps, so operators can trigger downstream refresh and rollout workflows after a successful renewal or deployment.
  • Delegated Renewal and Rotation Publish Controls — Renewal orchestration gained delegated execution paths, runtime probes, and publish-gate safeguards that better separate readiness validation from side-effecting deployment work.
  • Archive Center Lifecycle Enforcement — Archive restore and permanent delete flows are now treated as centralized archive-lifecycle operations with admin-only enforcement across runtime RBAC and role editing.
  • Expanded Agent Binary Coverage — MazeVault agent distribution now includes additional ARM64 coverage and stronger build validation for heterogeneous Windows and Linux estates.

Improvements

  • Discovered Certificate Key Custody Tracking — Adopted discovered certificates now preserve explicit key-custody state, preventing MazeVault from silently taking ownership of private keys during renewal or deployment flows.
  • Certificate Issuance and Renewal Robustness — Certificate issuance, CSR handling, and renewal flows now use stronger locking, normalized post-action payloads, and clearer actor attribution across cross-project secret operations.
  • CA Account Provider Coverage — CA account and EAB credential handling now covers more provider initialization paths, including Smallstep-specific fallback fields used during account creation and validation.

Bug Fixes

  • Smallstep CA Account Initialization — Smallstep CA account creation now correctly falls back to credential fields such as server_url and provisioner metadata instead of failing before contacting the CA.
  • Rotation Action Contract Alignment — Certificate post-action handling now exposes only action types supported by the certificate rotation executor, avoiding invalid UI selections and mismatched step names.

Version 1.0.43

Release Date: 2026-05-11

New Features

  • Azure Key Vault Managed Identity Support — Azure Key Vault integrations now support Managed Identity and Workload Identity as authentication methods in addition to service principals. When Azure Key Vault is selected in the Integration Wizard, the background sync auth method defaults to managed_identity. Configure a specific managed identity client ID via AZURE_MANAGED_IDENTITY_CLIENT_ID, or leave it unset to let the platform resolve the bound identity from the environment (AZURE_FEDERATED_TOKEN_FILE + AZURE_CLIENT_ID + AZURE_TENANT_ID for workload identity).
  • Agent Systemd Service Installation — The agent installer script now deploys the MazeVault agent as a proper systemd service. A dedicated mazevault-agent system user is created automatically, and locked-down directories are provisioned (/etc/mazevault, /var/lib/mazevault, /var/log/mazevault) with ownership and permissions (0750). The service is configured with automatic restart on failure. Existing deployments should re-run the deployment script to receive the updated service unit.
  • Agent Binary Download Endpoint — A new deployment-scoped endpoint GET /deployments/:id/agent-binary serves the agent binary for a given deployment. The deployment UUID acts as the authentication token (unguessable 128-bit identifier), consistent with the /script and /config endpoints. Legacy agent binary download rewrite rules in the reverse proxy have been removed.
  • Redis Multi-Endpoint Support — Redis connection handling now supports multiple endpoint addresses for high-availability and fallback setups. Configure the primary endpoint via REDIS_URL and one or more fallback addresses via REDIS_FALLBACK_URLS (comma, semicolon, or newline-separated). The client automatically falls back to the next available endpoint on connection failure, and switches to an in-memory fallback if all Redis endpoints are unavailable.
  • Password Complexity Policy in Organization Settings — Organization Settings now exposes a dedicated Password Complexity Policy editor. Rules are enforced at password change time (minimum length, character class requirements, history depth). On upgrade, organizations are automatically migrated from the legacy secret_complexity_policy field.

Improvements

  • Entra Credential Rotation History — Entra ID credential rotation operations now produce a persistent rotation history record for each event. The history captures the rotation type, old and new key identifiers, workflow and execution IDs, step-by-step progress, final status, error details if applicable, and elapsed duration. This enables full lifecycle traceability for Entra credential rotations.
  • RBAC Permission Alignment — Role permissions updated to match the agreed product policy:
    • secret_manager — gains keytab:read and config:read (KeyTab Management and Configuration Management pages are now accessible)
    • certificate_manager — gains config:read (Configuration Management page is now accessible)
    • auditor — keytab:read removed (KeyTab Management is no longer visible to auditors; assign explicit keytab:read where audit access to keytabs is required)
  • Admin Credential Reference Types Expanded — Database admin credentials now support two additional reference types: mazevault (credentials stored as MazeVault-managed secrets) and external (generic external references). Full set of supported types: internal, mazevault, external, keyvault, aws_sm.
  • Secret Sync Async Seeding with Status Summary — Sync seed operations now run asynchronously and return immediately with a progress reference and detailed status summary (secrets created, updated, skipped, failed). Blocked seed incidents are now surfaced on the Conflict Resolution page.
  • Certificate Issuance Linked to Agent — Certificates issued to a registered agent are now linked to the issuing agent record at issuance time, connecting the certificate lifecycle to the agent in the dashboard and rotation orchestration.
  • Security Enhancements — Bootstrap and KeyTab Handlers — KeyTab update and delete handlers now enforce organization ownership checks (IDOR prevention): access to a keytab belonging to a different organization returns 404. The bootstrap password change endpoint is locked after initial bootstrap completes and rejects further calls with 403 Forbidden. A DAST scan workflow has been added to the CI pipeline.
  • License Renewal Contact Updated — The license renewal contact email is now info@mazevault.com across all notification banners and expiry modals.

Bug Fixes

  • SSO Provider Modal: Provider ID Preserved on Save — Fixed a bug where saving an existing SSO provider configuration dropped the provider ID, causing duplicate provider creation on subsequent saves.
  • Password Complexity Policy Backfill — Organizations that had configured a secret complexity policy via the legacy secret_complexity_policy field were incorrectly evaluated as having no password policy, causing false-positive PCI-DSS 8.3 compliance failures. The policy is now automatically propagated to the dedicated password complexity field on upgrade. Additionally, organizations without any complexity policy now receive a secure default policy on upgrade (16-character minimum, all character classes required, 10-entry history, 30-day rotation interval).

Version 1.0.42

Release Date: 2026-05-11

New Features

  • Certificate Metadata Fields — Certificates now support three user-editable metadata fields accessible from the certificate detail view and via PUT /api/v1/certificates/:id:
    • Tags — Free-form string labels for grouping and filtering certificates. Tags set at import time are now persisted and remain editable after import. Send an empty array to clear all tags.
    • Documentation URL — Optional link (http/https) to external documentation, runbooks, or CMDB entries for the certificate. Maximum 500 characters.
    • Notes — Short free-text annotation (maximum 256 characters). Send an empty string to delete the note.
    • All metadata field changes are recorded in the audit log.

Improvements

  • Environment Canonicalization Enforced at Task Creation — Gateway task creation now enforces environment slug canonicalization. All gateway tasks are stored with lowercase environment slugs regardless of caller casing, building on the canonicalization infrastructure introduced in v1.0.41. The MAZEVAULT_ENV_CANONICAL_ENFORCE variable (set to true to fail-close on non-canonical inputs) applies to task creation as of this release.

Version 1.0.41

Release Date: 2026-04-26

Improvements

  • Documentation Sync — Comprehensive documentation update aligned with v1.0.41 codebase. Environment variable reference expanded with five new sections: License/Organization Registration, Orchestrator Mode, ACME DNS-01, KeyTab Management, and Agent Binary Proxy. All existing sections updated with previously missing variables.
  • Office 365 Email Variable Corrected — Fixed incorrect variable name O365_ENABLED → O365_EMAIL_ENABLED throughout documentation. Full Office 365 authentication configuration documented (client secret, certificate, and managed identity methods).
  • KeyTab API Reference — Full API documentation published for all 14 KeyTab management endpoints. See KeyTab API.
  • Reports API Reference — Full API documentation published for the Weekly Expiry Reports endpoints. See Reports API.
  • Platform Version Sync — All documentation pages updated to reflect current platform version.

Version 1.0.40

Release Date: 2026-04-25

New Features

  • OIDC Nonce Enforcement — New MAZEVAULT_ENFORCE_OIDC_NONCE flag enables strict nonce validation on OIDC tokens. When set to true, tokens without a valid nonce claim are rejected, providing protection against token replay attacks. Recommended for all production deployments.
  • Agent Trust Store Controls — New environment variables MAZEVAULT_AGENT_INSTALL_CHAIN_TO_TRUSTSTORE and MAZEVAULT_AGENT_TRUST_STORE_PATH control whether the MazeVault agent installs the internal CA certificate chain into the operating system trust store, and allow overriding the default trust store path on Linux.

Improvements

  • Certificate Rotation Target Sync Status — GET /api/v1/certificates/:id/targets/:targetId/status now returns full per-step result details, making failed target synchronizations easier to diagnose.
  • Gateway Registration Stability — Improved retry logic for bootstrap token exchange reduces failed registrations caused by transient network issues during first-time gateway setup.
  • OCSP URL Validation — OCSP_URL backend variable now validates URL format at startup to prevent misconfiguration from silently causing OCSP failures.

Bug Fixes

  • Corrected AGENT_VERSION=latest resolution to always fetch the highest tagged release version rather than the most recent commit.
  • Fixed display overlap in the certificate import modal under Orchestrator Mode when both keytab and private key sections were visible simultaneously.

Version 1.0.39

Release Date: 2026-04-22

New Features

  • Agent Binary Distribution Control — New configuration variables provide granular control over how agent updates are distributed across the fleet. AGENT_ROLLOUT_PERCENTAGE limits what percentage of agents receive update notifications (0–100), enabling staged rollouts. AGENT_MAX_CONCURRENT_DOWNLOADS caps parallel binary download streams to prevent network saturation.
  • Agent Binary Proxy — MazeVault can now proxy agent binary downloads from a private GitHub release repository, removing the requirement for agent hosts to reach the public GitHub Releases endpoint directly. Configure via AGENT_BINARY_GITHUB_TOKEN, AGENT_BINARY_CACHE_DIR, AGENT_DOWNLOAD_BASE_URL, and AGENT_VERSION.
  • Primary Backend Environment Seeding — New MAZEVAULT_PRIMARY_ENVIRONMENTS variable pre-seeds the list of environments served directly by the primary backend on first startup, simplifying initial deployment configuration of multi-environment setups.

Improvements

  • KeyTab Dashboard Refresh — Fixed cipher compliance breakdown chart not updating after importing a keytab with deprecated ciphers.
  • Weekly Report Multi-Channel Reliability — Resolved a scheduling race condition that could silently drop one delivery channel when multiple channels were all enabled simultaneously.

Bug Fixes

  • Fixed gateway heartbeat timestamp not updating correctly following a network partition recovery.

Version 1.0.38

Release Date: 2026-04-19

New Features

  • KeyTab Management — Full Kerberos Lifecycle — Enterprise-grade Kerberos KeyTab management with complete lifecycle support. Import, discover, and manage keytab files across your infrastructure. Key capabilities include:
    • Import & Parse — Import MIT Kerberos v2 keytab binary files with automatic extraction of principals, realms, key version numbers (KVNO), and encryption types. Supports base64-encoded upload.
    • Agent Discovery — Agents automatically discover .keytab files on managed hosts, reporting file path, permissions, owner, and encryption type fingerprint. Discovered keytabs can be imported into managed inventory with a single action.
    • Cipher Policy Enforcement — Define organization-level cipher policies specifying allowed and deprecated Kerberos encryption types. Three enforcement modes: audit (report only), warn (allow with warning), block (prevent non-compliant keytabs). Default policy blocks legacy ciphers (DES, RC4-HMAC) while allowing modern AES and Camellia ciphers.
    • Version History — All keytab updates create immutable version records with change reason tracking for full audit compliance.
    • Dashboard & Analytics — Dedicated KeyTab dashboard showing total/active/expired counts, cipher compliance breakdown (compliant/warning/critical), expiry forecasts, and cipher type distribution.
    • Orchestrator Mode Support — In Orchestrator Mode, keytab binary data is offloaded to an external provider; only metadata is stored locally.
  • Weekly Expiry Report — Automated weekly reports showing certificates and secrets expiring within 60 days, delivered to multiple channels simultaneously:
    • Email — HTML-formatted report to configured recipient list
    • Slack / Microsoft Teams — Webhook-based notifications with expiry summaries
    • JIRA — Automatic issue creation with expiry details for tracking
    • Generic Webhook — HTTP POST with full report payload for custom integrations
    • Reports can be previewed before sending and triggered manually on demand.
  • Local Gateway Registration — The primary backend can now register itself as a local gateway, enabling unified gateway management UI for both local and remote gateways. A unique constraint ensures only one local gateway per deployment.

Improvements

  • Gateway Multi-Environment Support — Gateways can now serve multiple environments simultaneously, removing the previous one-gateway-per-environment restriction.
  • Report RBAC Permissions — New report:read and report:write permissions provide fine-grained access control for the reporting system. All standard roles (User, Certificate Manager, Secret Manager, Auditor) receive report:read; Admin and Project Admin additionally receive report:write.
  • KeyTab RBAC Permissions — New keytab:read, keytab:write, keytab:delete, and keytab:admin permissions control access to keytab management. Standard users and auditors receive read access; operators and organization admins receive write and delete; organization admins additionally receive admin access for policy management.

Bug Fixes

  • Entra Sync Rule Cleanup — Removed orphaned sync rules left behind by deleted integrations, and cleaned up duplicate sync rules for entra_id provider type (now handled by the dedicated Entra Sync Scheduler). Fixes recurring "sync failed for rule" errors in production environments.

Version 1.0.37

Release Date: 2026-04-16

Improvements

  • Gateway Task Payload Encryption — Sensitive task payloads exchanged between the primary backend and gateways are now encrypted at rest in the database. The payload_encrypted flag on gateway tasks ensures that JSONB payloads containing credentials and private keys are protected even if database access is compromised.
  • Write Queue Exponential Backoff — Multi-datacenter write queue now tracks the timestamp of each retry attempt, enabling proper exponential backoff calculation for failed synchronization operations. This improves reliability and reduces unnecessary load on remote gateways during connectivity disruptions.

Security Updates

  • Gateway Payload At-Rest Encryption — Task results and payloads in the gateway task queue are now encrypted before database storage, closing a potential data exposure vector in multi-datacenter deployments.

Version 1.0.36

Release Date: 2026-04-15

New Features

  • KeyTab Database Schema — New database tables for keytab management: keytabs (encrypted keytab storage with cipher compliance tracking), keytab_versions (immutable version history), keytab_cipher_policies (organization-level cipher enforcement), and discovered_keytabs (agent discovery results with stale detection).
  • KeyTab RBAC Permissions — New permission set (keytab:read, keytab:write, keytab:delete, keytab:admin) assigned to appropriate system roles for keytab lifecycle management.

Improvements

  • Gateway API Token Enhancement — Improved gateway authentication with dedicated API tokens and bootstrap provisioning support.
  • Agent KeyTab Discovery — Agents can now discover Kerberos keytab files on managed hosts and report findings including file path, permissions, owner, encryption types, and SHA-256 fingerprint.

Version 1.0.35

Release Date: 2026-04-14

Bug Fixes

  • PEM Import — Private Key Preservation — Fixed a critical issue where importing a PEM file containing a certificate chain and a private key would silently discard the private key. The system correctly detected the key during file preview but lost it during the actual import, causing subsequent PFX/JKS exports to fail. The PEM bundle parser now correctly extracts PKCS#8, RSA, and EC private key blocks.
  • Certificate Import — Project Name in Error Messages — When importing a certificate that already exists, the error message now includes the project name where the duplicate resides (e.g., "certificate already exists … project=MyProject"), making it easier to identify conflicts.

Improvements

  • Private Key Visibility in UI — Certificates now display their private key status across all views:
    • Certificate lists show a green shield icon when a private key is stored.
    • The certificate dashboard shows a green key icon next to certificates with private keys.
    • The certificate detail modal displays a chip indicating whether the key is stored locally, externally, or not available.

Version 1.0.34

Release Date: 2026-04-10

New Features

  • Identity Provider Group Discovery — New API endpoint GET /identity-providers/{id}/groups fetches groups directly from the configured identity provider (Entra ID via Microsoft Graph, LDAP via directory search). Supports search filtering by group display name for easy role mapping.
  • Identity Provider Test Coverage — Added comprehensive unit test coverage for identity provider CRUD operations, test-connection flow, and group discovery endpoints.

Improvements

  • JKS Export — Pure Go Implementation — Replaced the external keytool (JDK) dependency with a native Go implementation using keystore-go/v4. JKS export now works in any environment without requiring a Java runtime, includes the full certificate chain, and has comprehensive test coverage.
  • Gateway API Token Authentication — New gateway_api_tokens table and middleware for gateway-to-backend API authentication. Gateways can now authenticate using dedicated API tokens with automatic bootstrap provisioning.
  • Gateway Write Queue — Added gateway_write_queue table for buffering write operations from gateways, enabling reliable data synchronization in multi-datacenter deployments.
  • Gateway Bootstrap Hardening — Improved gateway bootstrap flow with enhanced validation, Azure SQL connectivity checks, and more reliable initial registration.
  • Azure Test Environment Terraform — New Terraform configuration for automated Azure test environment provisioning, including Entra ID enterprise apps, Key Vaults, and Azure SQL.

Security Updates

  • Gateway Middleware Authentication — New dedicated middleware validates gateway API tokens with proper scope checks and request context propagation.

Version 1.0.33

Release Date: 2026-04-09

New Features

  • Azure Permissions Check Endpoints — Added new Azure access validation APIs for permissions and resource visibility:
  • POST /api/v1/admin/azure/mi-permissions-check
  • GET /api/v1/azure/user-permissions-summary
  • GET /api/v1/azure/subscriptions/{subscriptionId}/sql-servers
  • Managed Identity Permissions Validation — New managed identity check flow validates access across configured Azure integrations and returns per-integration status results.
  • User Permissions Summary — Added a consolidated subscription-level overview of Azure resources visible to the authenticated user, including Key Vault and SQL server discovery.

Improvements

  • RBAC Integration for Azure Permission Checks — Azure permissions endpoints are now protected by MazeVault RBAC with integration:read and integration:write guards.
  • Swagger Schema Coverage — OpenAPI definitions now include the Azure permissions check response models (MIPermissionsCheckResponse, MICheckResult) for accurate API client generation.

Security Updates

  • Role Permission Alignment — Migration 000109_add_audit_settings_permissions adds audit:read and project:write permissions to certificate_manager and secret_manager roles for consistent access control behavior.

Version 1.0.32

Release Date: 2026-04-08

New Features

  • Azure Managed HSM Integration — Full support for key storage and manipulation in Azure Managed HSM with automatic certificate updates and key rotation. All private key operations occur in the HSM with metadata returned to the database.
  • Organization-Level Password Policy — Define password enforcement rules (minimum length, complexity, expiry) for all organization users. Rules are enforced at password creation and change with legacy integration compatibility.

Improvements

  • CRDT Sync Performance — Optimized conflict resolution for datasets >100k records, 40% speed improvement on multi-DC setups.
  • Extended Audit Logs — Comprehensive recording of all password, certificate, and administrative operations.

Bug Fixes

  • Fixed cache invalidation timing in cluster deployments.
  • Resolved sync failures with large CSR transactions.

Version 1.0.31

Release Date: 2026-04-07

New Features

  • Orchestrator Mode — External Key Storage — MazeVault can now run in a mode where all private keys and secrets are stored exclusively in external key vaults (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault). Local database contains only metadata.
  • License Compliance Tracking — Dashboard displays license status in real-time, including used licenses, upcoming expirations, and warning states.

Improvements

  • Standard → Orchestrator Mode Migration — New CLI commands for safely migrating existing Standard mode systems to Orchestrator mode.
  • Orchestrator Mode UI — Toggle in Onboarding Wizard and organization settings.

Bug Fixes

  • Fixed selective certificate copying in Orchestrator mode.

Version 1.0.30

Release Date: 2026-04-07

New Features

  • ACME Server — Full RFC 8555 Implementation — MazeVault now acts as a fully compliant ACME certificate authority. Any standard ACME client (cert-manager, Certbot, acme.sh, Kubernetes) can obtain and renew certificates directly from MazeVault without manual intervention. Supported challenge types: HTTP-01, DNS-01, TLS-ALPN-01.
  • External Account Binding (EAB) — Restrict ACME account registration to authorized clients using pre-shared EAB credentials (HMAC-based). Prevents unauthorized certificate issuance from unknown ACME clients.
  • Sync Dashboard — New dashboard section showing real-time synchronization status: configuration overview, active conflicts, and sync failures with per-item resolution guidance.
  • Sync Read Permission — New sync:read permission grants project members, auditors, and role holders access to sync status endpoints without requiring elevated privileges.

Improvements

  • ACME Authorization Nonce Hardening — Dedicated nonce table (acme_server_nonces) with TTL-based expiry ensures strict replay-nonce protection per RFC 8555 §6.5.
  • ACME Challenge Token Indexing — Challenge tokens now use a dedicated indexed column for O(1) validation lookups instead of JSON scanning, improving throughput under high ACME request volume.
  • RBAC — Sync Permissions Aligned — project_admin, certificate_manager, secret_manager, user, and auditor roles now include sync:read for consistent access to sync dashboard endpoints.

Bug Fixes

  • Fixed race condition in ACME order state transitions during concurrent finalize requests.
  • Resolved ACME authz table creation ordering issue (migration idempotency).

Version 1.0.29

Release Date: 2026-04-06

New Features

  • Configuration Management Interface — New multi-tab UI for lifecycle management of configuration files: discover YAML/JSON/INI files across environments, stage drafts, and promote configurations to production. Provides visibility into discovered vs. managed configuration counts.
  • Certificate Rotation Polymorphic Config — Rotation executions now support both secret and certificate rotation configs in a unified model. The config_type field distinguishes between the two, eliminating separate workflow tables.
  • Rotation Settings Source Tracking — Each certificate now records whether its renewal settings originate from a project template (template) or were manually configured (manual), providing clear audit attribution for rotation behavior.

Improvements

  • Organization Scoping for Certificate Requests — Certificate signing requests (CSRs) are now scoped to the originating organization. The organization_id field is backfilled from project associations, preventing cross-organization CSR data leakage.
  • ACME Server Authorization Tables — Database schema for the ACME server protocol (authorization objects and nonce tables) was provisioned in preparation for the v1.0.30 ACME server release.
  • Rotation Workflow Cleanup — Removed orphaned rotation_workflows and rotation_step_executions tables that were superseded by the unified rotation execution model.

Bug Fixes

  • Fixed rotation_configs.next_rotation column rename to next_rotation_at — scheduler queries now use the correct column name.
  • Resolved foreign key constraint on rotation_executions.config_id that prevented polymorphic rotation config references.

Version 1.0.28

Release Date: 2026-04-05

New Features

  • Secret Naming Policies — Define organization-wide naming conventions for secrets using regex-based rules with three enforcement levels: block (prevent creation), warn (allow with warning), and disabled (informational). Policies are managed as configuration templates and validated in real-time on secret creation.
  • Naming Compliance Dashboard — New tab in the Secrets Dashboard shows a policy violation heatmap, per-rule compliance rates, and auto-generated rule suggestions based on existing secret naming patterns.
  • Consistency Framework — Create consistency groups to verify that specified secrets exist across all required environments. The POST /projects/{id}/consistency/groups endpoint and dashboard tab surface missing values and environment gaps, with resolve-warning support for documented exceptions.
  • Database Security Defaults — Organizations can now configure per-environment TLS/encryption baselines for database integrations. Production environments default to strict TLS (verify-full, encrypt, TCPS); non-production environments use permissive defaults. Supported providers: Oracle, MSSQL, PostgreSQL, MongoDB, MySQL.
  • Enhanced Shared Secrets — Shared secrets now support optional passphrase protection (bcrypt), recipient_email for intent tracking, automatic content-type tagging (secret or certificate), and rotation source attribution (source_type, source_id) for automated post-rotation distribution.

Security Updates

  • Consistency RBAC Permissions — New consistency:read and consistency:write permissions control access to consistency groups and warning resolution. Assigned to project_admin, certificate_manager, secret_manager, user, and auditor roles.
  • Auditor Role Refinement — The auditor role no longer has access to the deployment dashboard or general dashboard views, focusing the role strictly on audit logs, user management, gateway status, and agent status — reducing the attack surface of read-only accounts.

Bug Fixes

  • Fixed secret naming policy storage: policies are now persisted in config_management_templates instead of the deprecated organizations.secret_complexity_policy JSONB column.

Version 1.0.27

Release Date: 2026-04-04

New Features

  • New Roles: Certificate Manager & Secret Manager — Two new purpose-built roles provide fine-grained access control without granting cross-domain visibility:
  • certificate_manager — Full lifecycle management for certificates, CA accounts, templates, discovery, and deployment. No access to secrets.
  • secret_manager — Full lifecycle management for secrets, rotation, deployment, and integrations. No access to certificates.
  • SSO Authorization Code Pattern — Access tokens are no longer passed via URL query parameters on SSO callback. The callback now delivers a short-lived opaque code (sso_code, 60-second TTL) that the frontend exchanges for tokens via POST /auth/sso/exchange. This eliminates token exposure in browser history, server access logs, and Referer headers. Applies to Entra ID, GitHub, and GitLab SSO providers.
  • Entra Group Mapping Consolidation — Entra ID group-to-role mappings are now stored in the unified group_role_mappings table with a source column (local or entra). This enables consistent group management across local LDAP groups and Entra ID cloud groups from a single interface.

Security Updates

  • OIDC Nonce Validation — OAuth2 state now carries a server-generated nonce validated at token exchange. Prevents CSRF/token-hijacking via cross-site OAuth state substitution.
  • Deprecated Roles Removed — Legacy roles (operator, developer, org_admin, secret_editor, secret_viewer, certificate_admin, system_admin) have been removed. Existing users with these roles were automatically migrated: operator → project_admin, developer → user. Integrations using deprecated role names in API calls must be updated.
  • User Role Data Integrity — Fixed zero-UUID primary key corruption in the user_roles table caused by a missing BeforeCreate hook. A composite unique index was added to prevent duplicate role assignments. Affected rows were deduplicated during migration.
  • Soft-Deleted User Email Index — The email uniqueness constraint is now a partial index (WHERE deleted_at IS NULL). Soft-deleted users no longer block new account creation or SSO registration with the same email address.

Bug Fixes

  • Fixed Entra group mapping duplication after SSO re-authentication.
  • Resolved role assignment failures for users created via LDAP group sync.

Version 1.0.26

Release Date: 2026-04-03

New Features

  • PFX/PKCS#12 Import Improvements — Enhanced PFX certificate import with improved parsing and validation
  • Certificate Template Fixes — Resolved template configuration issues affecting certificate enrollment workflows
  • Role Mapping Modifications — Updated group-to-role mapping logic for improved SSO integration
  • Audit Log Enhancements — Extended audit logging with additional event types and improved traceability

Security Updates

  • Comprehensive Vulnerability Remediation — Resolved 18 out of 24 identified vulnerabilities across all platform components through systematic dependency scanning and updates
  • SAML Signature Bypass Fix (CRITICAL) — Fixed critical SAML XML signature bypass vulnerability in SSO authentication flow (goxmldsig v1.3.0 → v1.6.0)
  • HTTP/2 CONTINUATION Flood Fix (CRITICAL) — Resolved actively exploitable HTTP/2 denial-of-service vulnerability in Kubernetes Operator (golang.org/x/net v0.19.0 → v0.52.0)
  • gRPC Authorization Bypass Fix — Fixed gRPC authorization bypass via missing leading slash in path (google.golang.org/grpc → v1.80.0)
  • JOSE/JWE Denial-of-Service Fixes — Resolved 3 separate DoS vulnerabilities in JSON Web Encryption handling (go-jose/v3 v3.0.0 → v3.0.4)
  • Node.js SDK Critical Fix — Eliminated 8 critical Handlebars.js advisories including JavaScript injection and prototype pollution
  • CI/CD Security Hardening — Pinned Trivy security scanner to specific version (supply chain protection), enabled security scanning on all CI events

Improvements

  • Kubernetes Operator Overhaul — Major dependency update to controller-runtime v0.22.5 and k8s.io/* v0.34.3 with code quality improvements including extracted reconciliation methods, configurable refresh intervals, proper watch propagation, and structured logging
  • Terraform Provider Fix — Fixed compilation error and updated all dependencies to latest stable versions (terraform-plugin-framework v1.19.0, grpc v1.80.0)
  • Go SDK Enhancement — Added Environment field to Project model for improved project management
  • Docker Image Hardening — Pinned OCSP Responder base image to alpine:3.21 (reproducible builds), switched Frontend to npm ci for deterministic dependency installation
  • Dependency Alignment — Aligned golang.org/x/crypto, golang.org/x/net, and other standard library packages across all 7 Go modules to latest stable versions

Version 1.0.25

Release Date: 2026-04-01

New Features

  • Swagger API Documentation Overhaul — Comprehensive regeneration of Swagger/OpenAPI documentation with complete endpoint coverage, improved schema definitions, and accurate request/response examples
  • Entra ID SSO Environment Configuration — New environment variables for Entra ID SSO and Azure Managed Identity configuration in .env.example for streamlined deployment setup

Improvements

  • LDAP & OAuth Provider Configuration — Enhanced SSO provider setup with improved LDAP bind DN handling and OAuth2 flow configuration
  • Email Status Endpoint — New GET /api/v1/system/email-status endpoint for monitoring email notification delivery status
  • Certificate Rotation Handlers — New API handlers for certificate rotation execution and status tracking
  • Agent Integration Enhancements — Improved agent discovery and integration handlers with better error reporting
  • SSH Key Management — Extended SSH key service with improved import and rotation capabilities

Security Updates

  • Updated authentication service with enhanced token validation and session management
  • Improved Entra ID Graph client with additional security headers

Version 1.0.24

Release Date: 2026-03-30

Bug Fixes

  • Entra ID SSO Fix — Resolved critical Entra ID SSO authentication issue affecting login flow and token refresh
  • Code Cleanup — Removed deprecated handler code and unused Entra mapping endpoints for cleaner codebase

Improvements

  • Updated schema models with additional field definitions for improved data integrity

Version 1.0.23

Release Date: 2026-03-30

Improvements

  • Certificate Templates and Expiry Management — Enhanced certificate template configuration with improved expiry tracking, scheduler optimizations, and better CA integration status indicators
  • Certificate Import Validation — Improved certificate import service with stricter chain validation and better error messages
  • SSLmarket CA Sync — Extended SSLmarket CA provider with improved product synchronization and certificate status tracking
  • Extended Logging — Enhanced logging across certificate services for better troubleshooting and audit trail

Bug Fixes

  • Fixed certificate status calculation in X.509 utility functions
  • Resolved certificate dashboard display issues for expiring certificates
  • Fixed certificate edit modal preserving incorrect values on save

Version 1.0.22

Release Date: 2026-03-30

New Features

  • Multi-Gateway Environment Support — New database migration and service layer for multi-gateway deployments with environment-specific gateway configuration, health monitoring, and task execution
  • Gateway Health Monitor — Real-time gateway health monitoring service with heartbeat tracking, automatic failover detection, and Prometheus metrics
  • Gateway Routing Service — Intelligent request routing across multiple gateway instances with load balancing and environment awareness
  • Gateway Task Executor — Distributed task execution framework for gateway operations with retry logic and status tracking
  • Identity Provider Management — New API handlers for identity provider configuration and management

Improvements

  • Cipher Key Resolver Hardening — Comprehensive test coverage for cipher key resolution with 715+ lines of new tests eliminating dual storage inconsistencies
  • Database Health Checks — Updated expected tables and columns for new gateway-related database schema
  • Integration Wizard — New multi-step integration wizard UI for configuring CA providers, secret managers, and external integrations
  • Key Derivation Fix — Resolved key derivation issue affecting encryption operations

Security Updates

  • Eliminated cipher key dual storage vulnerability via migration 000089
  • Enhanced authentication service with improved session handling

Bug Fixes

  • Fixed test failures in certificate orchestrator and configuration management services
  • Resolved EntraID SSO redirect issue on certain browser configurations

Version 1.0.21

Release Date: 2026-03-26

New Features

  • Azure Resource Discovery — New API handlers for Azure cloud resource discovery with Key Vault, certificate, and secret enumeration
  • Integration Wizard UI — Multi-step wizard for configuring integrations with CA providers and secret managers, including type selection, provider configuration, and review steps

Improvements

  • CA Account Service — Enhanced CA account management with improved error handling and status tracking
  • Model Schema Updates — Updated data models across SSH keys, MFA, OAuth, CRL, and zero-trust modules for improved consistency and validation
  • Test Coverage — Expanded test coverage for integration service providers, key offload service, and secret service offload operations

Bug Fixes

  • Fixed certificate audit event model inconsistency
  • Resolved discovered certificate model field alignment issues

Version 1.0.20

Release Date: 2026-03-23

New Features

  • Office365 OAuth2 Email Notifications — Send email notifications via Microsoft Graph API using OAuth2 client credentials flow instead of legacy SMTP; supports 3 authentication methods (client secret, certificate, managed identity); reuses existing Entra ID infrastructure with connection caching and retry logic; transparent replacement — all 6 email trigger points (expiry alerts, incidents, weekly reports, rotation failures, discovery summaries, test notifications) work automatically; new GET /api/v1/system/email-status endpoint and frontend status indicator in System Outputs → Notifications tab
  • SmallStep CA Provider — Integration with open-source step-ca as a Certificate Authority backend supporting JWK, X5C, and OIDC provisioners; mTLS authentication, certificate signing, renewal, revocation, and CRL signing with root fingerprint verification; ideal for zero-trust short-lived certificate architectures
  • Multi-Target Certificate Rotation — Deploy certificates to 5 destination types: Secret Managers (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault), Kubernetes Secrets (TLS/Opaque), Agent Keystores (JKS, PKCS12, Windows), Agent Files (PEM), and Database Wallets (Oracle OCI); content mode selection (public only, public+chain, full chain with key, key only), format auto-detection, retry policies with configurable attempts and delays, and post-install command execution
  • Enterprise Entra ID Credential Lifecycle — Complete lifecycle management with states (created, active, expiring, expired, revoked, grace period), configurable grace periods (default 30 days), rotation history tracking with old/new key IDs and workflow tracing, expiry monitoring dashboard, idempotency protection against duplicate rotations, and sync conflict resolution for local vs. remote state
  • Compliance Report Viewer — Generate and view compliance reports with template-based formatting, organization-wide certificate compliance analysis, and exportable report output
  • Rotation Execution History — Detailed per-certificate rotation history with execution timestamps, status tracking (success/failed/pending), and target-level sync status visualization
  • Certificate Lifecycle Phase Tracking — New lifecycle phases (stable, renewing, rotating, revoking) prevent duplicate CA requests during in-flight operations; orthogonal to certificate status, ensuring certificates remain valid during phase transitions

Improvements

  • Audit Stream Destinations — New Elasticsearch destination with index template management, cluster mode and CosmosDB support; new Syslog destination with TCP/UDP transport, CEF and Syslog format support; enhanced log stream service with destination-specific configuration validation
  • LDAP Authentication Improvements — Enhanced LDAP service with improved bind DN handling, group membership resolution, schema configuration flexibility, and better error diagnostics for connection failures
  • Notification Scheduler Redesign — Improved scheduling logic for certificate expiry notifications with batching support to reduce alert fatigue, JIRA integration for incident ticket creation, and email notification enhancements
  • Weekly Expiry Report Service — Redesigned report generation with recipient management, customizable report content, and improved delivery reliability
  • Project Template Enhancements — Extended template configuration with naming convention integration, advanced default settings, and improved template-to-project application workflow
  • Naming Convention Service — Extended naming convention engine with additional pattern support, validation rules, and convention-to-template linking for automated enforcement
  • Prometheus Metrics — New histogram and counter metrics for certificate rotation, compliance reporting, and audit stream performance monitoring
  • CI/CD Pipeline — New GitHub Actions workflow for automated build, test, and deployment
  • SSO Provider Modal — Added LDAP provider configuration with server, bind DN, and schema settings directly from the SSO configuration interface
  • WebLogic Deploy Rotation Step — New rotation step type for Oracle WebLogic Server keystore deployment with automated domain configuration updates

Security Updates

  • Entra ID credential rotation with full audit trail — every rotation recorded with actor, timestamp, old/new key IDs, and execution status
  • SmallStep CA operations logged to audit stream with complete request/response metadata
  • Certificate lifecycle phase prevents concurrent CA operations, eliminating race conditions in renewal and rotation workflows
  • Entra sync conflict detection with automatic tracking of local vs. remote state discrepancies

Version 1.0.19

Release Date: 2026-03-20

New Features

  • Azure Gateway Deployment — Multi-region Azure Gateway infrastructure with Terraform modules for AKS, Key Vault, PostgreSQL, Redis, networking, monitoring, and identity management; Helm chart values for gateway configuration; Azure DevOps CI/CD pipeline for automated deployment
  • Key Vault RBAC Service — Granular role-based access control for Azure Key Vault operations with per-secret and per-certificate permission management
  • Key Vault Watch Connector — Real-time synchronization between MazeVault and Azure Key Vault with change detection and automatic secret updates
  • Naming Convention Engine — Domain-specific naming rules with wildcard pattern matching, priority ordering, and template-level enforcement for consistent resource naming across projects
  • Dashboard Reports Tab — Redesigned reporting dashboard with certificate overview statistics, interactive charts, and exportable report data
  • Node.js SDK — Official MazeVault SDK for Node.js with TypeScript definitions, supporting authentication (SRP), organizations, projects, and secrets management

Improvements

  • Audit Log Project Scoping — Audit events now include project association for efficient per-project filtering and compliance reporting; existing events backfilled from entity relationships
  • Orchestrator Storage Mode Fix — Normalized inconsistent storage mode values for Orchestrator Mode deployments, resolving constraint violations
  • Entra Group Mappings — Improved group-to-role mapping reliability with better error handling and UI feedback
  • Config Encryption Hardening — Enhanced configuration encryption service with additional test coverage and improved error handling for edge cases
  • Certificate Import Validation — Improved certificate import service with stricter chain validation
  • Go SDK Updates — Updated project and model definitions for consistency with latest API
  • Python SDK Updates — Model alignment with latest API schema

Bug Fixes

  • Fixed environment creation for unused projects being triggered unnecessarily
  • Resolved Entra ID SSO login redirect issue on certain browser configurations
  • Fixed project template modal not preserving environment associations on save

Version 1.0.18

Release Date: 2026-03-14

Improvements

  • Customer Documentation Updated — Comprehensive ACME certificate automation guide with step-by-step Kubernetes setup, cert-manager ClusterIssuer examples, ACME profile routing, troubleshooting, and complete end-to-end YAML examples; updated Certificates API reference with ACME endpoints and EAB management; release notes reformatted to correct 1.0.x versioning scheme
  • Azure Entra ID Integration Improved — Enhanced token refresh handling, improved group-to-role mapping reliability, faster SSO login flow with reduced redirect latency, and better error messages for misconfigured tenant settings

Version 1.0.17

Release Date: 2026-03-14

New Features

  • ACME Server (RFC 8555) — MazeVault now acts as a full ACME Certificate Authority, enabling automated certificate issuance via cert-manager and other ACME clients
  • External Account Binding (EAB) — Secure cluster registration with one-time-use credentials linking ACME clients to organizations and projects
  • ACME Profile Routing — Map cert-manager profile names to MazeVault Certificate Templates for automatic CA backend selection (cert-manager v1.18+)
  • Domain Rule Engine — Configure domain-to-template routing rules with wildcard pattern matching and priority ordering
  • Auto-Approve for Internal Domains — Certificates for .local, .internal, .lan, and .corp domains are issued instantly without HTTP-01 challenge
  • ADCS Bridge via ACME — Issue certificates from Microsoft Active Directory Certificate Services through standard ACME protocol
  • ADCS Agent Improvements — DCOM retry logic for pending certificate requests with configurable intervals
  • EAB Credential Management UI — Generate, list, and revoke EAB credentials from the web interface with cert-manager YAML examples

Improvements

  • ACME directory endpoint with meta profiles for automated client discovery
  • JWS middleware with ES256 and RS256 signature verification
  • Nonce-based replay protection per RFC 8555 §6.5
  • Full PEM certificate chain delivery for ACME clients
  • EAB credentials table with status tracking (Available / Used / Revoked)
  • One-click copy for ACME directory URL and generated credentials

Security Updates

  • EAB HMAC keys encrypted at rest with AES-256-GCM
  • EAB credentials are single-use and support expiration and revocation
  • JWK Thumbprint verification (RFC 7638) for account binding
  • All ACME operations recorded in audit log

Version 1.0.16

Release Date: 2026-02-28

Improvements

  • Organization settings redesign with tabbed navigation
  • Certificate Authority account cards with sync status indicators
  • Improved CA product discovery and sync trigger via UI
  • Agent heartbeat interval optimization for large fleets
  • Database connection pool tuning for high-concurrency deployments

Bug Fixes

  • Fixed certificate chain validation for intermediate CA certificates
  • Resolved race condition in concurrent secret rotation scheduling
  • Fixed OCSP responder cache invalidation on certificate revocation

Version 1.0.15

Release Date: 2026-02-14

Improvements

  • PFX/PKCS#12 import with configurable key storage (software / HSM)
  • Certificate template override support for CA account-level defaults
  • Improved agent reconnection logic after network interruptions
  • Enhanced audit log filtering by event type and date range

Bug Fixes

  • Fixed certificate export with chain for cross-signed intermediates
  • Resolved project template settings not persisting after save
  • Fixed SSH key discovery deduplication for rotated keys

Version 1.0.14

Release Date: 2026-01-31

Improvements

  • SSH key management enhancements — authorized key tracking and discovery
  • Rotation trigger improvements with foreign key constraint handling
  • Secret sharing post-rotation with automatic re-encryption
  • Improved error messages for agent proxy authentication failures

Bug Fixes

  • Fixed rotation scheduler timezone handling for non-UTC installations
  • Resolved dashboard certificate count discrepancy after bulk import
  • Fixed API token expiration check for service identities

Version 1.0.13

Release Date: 2026-01-24

Improvements

  • React import modernization — tree-shaking optimized MUI imports
  • Frontend build size reduction (~15% smaller bundle)
  • Improved certificate search with wildcard SAN matching
  • Enhanced monitoring Prometheus metrics with histogram buckets

Bug Fixes

  • Fixed RBAC permission check for nested project environments
  • Resolved Terraform export formatting for complex secret values
  • Fixed health check endpoint returning stale Redis status

Version 1.0.12

Release Date: 2026-01-17

Improvements

  • Trivy container image scanning integration in CI/CD pipeline
  • Security vulnerability remediation for dependency chain
  • Improved TLS cipher suite configuration with Mozilla Intermediate profile
  • Enhanced rate limiting with sliding window algorithm

Bug Fixes

  • Fixed certificate renewal scheduling for certificates with custom validity
  • Resolved sync conflict for simultaneously edited secrets across datacenters
  • Fixed agent registration token validation for re-registered agents

Version 1.0.11

Release Date: 2026-01-10

Improvements

  • PostgreSQL connection health monitoring with automatic reconnection
  • Bidirectional sync architecture improvements for multi-region deployments
  • Agent installation script fixes for air-gapped environments
  • Onboarding flow improvements for remote deployment scenarios

Bug Fixes

  • Fixed database migration rollback for failed upgrades
  • Resolved LDAP group sync not reflecting membership changes
  • Fixed certificate list pagination for projects with > 1000 certificates

Version 1.0.10

Release Date: 2026-01-03

Improvements

  • Enhanced external change detection for CA-managed certificates
  • Improved Helm chart values documentation with inline comments
  • Certificate expiry notification batching to reduce alert fatigue
  • Updated Go dependencies with security patches

Bug Fixes

  • Fixed CRDT merge for concurrent secret version creation
  • Resolved Azure Key Vault sync retry logic for transient failures
  • Fixed OCSP responder returning incorrect status for renewed certificates

Version 1.0.9

Release Date: 2025-12-15

New Features

  • Certificate Template System — Pre-configured certificate profiles for common use cases (Web Server, Client Auth, Code Signing, Email/S-MIME)
  • Bulk Certificate Operations — Import and manage certificates in bulk via PEM bundles
  • Enhanced Agent Discovery — Automatic discovery of certificates across agent-managed infrastructure
  • Sync Improvements — CRDT-based multi-datacenter synchronization with improved conflict resolution
  • Terraform Export — Export project configurations as Terraform HCL for infrastructure-as-code workflows

Improvements

  • Improved certificate import validation and error reporting
  • Enhanced OCSP responder performance with response caching
  • Updated RBAC with granular certificate management permissions
  • Improved audit logging with structured JSON output
  • Enhanced health check endpoints with component-level status

Security Updates

  • TLS 1.3 as default protocol
  • Improved CSRF protection with double-submit cookie pattern
  • Enhanced rate limiting with per-endpoint configuration
  • Updated cryptographic dependencies

Version 1.0.8

Release Date: 2025-11-20

New Features

  • External CA Integration — Connect to DigiCert, Venafi, Microsoft ADCS, and other external Certificate Authorities
  • HSM Support — Hardware Security Module integration for key protection (PKCS#11, Azure Managed HSM)
  • ACME Protocol — Automated Certificate Management Environment for automated certificate issuance
  • Multi-Factor Authentication — TOTP-based MFA for enhanced account security

Improvements

  • Redesigned certificate management interface
  • Improved secret rotation scheduling
  • Enhanced API rate limiting
  • Expanded Azure Key Vault integration

Version 1.0.7

Release Date: 2025-09-10

New Features

  • Zero-Knowledge Encryption — Client-side encryption for personal vault secrets
  • SCEP Protocol Support — Simple Certificate Enrollment Protocol for device certificate management
  • EST Protocol Support — Enrollment over Secure Transport for modern certificate enrollment
  • Agent Proxy — Agents can proxy secret access for local applications

Improvements

  • Improved database migration system
  • Enhanced logging and monitoring
  • Updated Kubernetes deployment manifests
  • Performance improvements for large certificate stores

Version 1.0.6

Release Date: 2025-06-15

New Features

  • Multi-Datacenter Sync — Bidirectional synchronization between MazeVault installations
  • Azure Entra ID SSO — Single Sign-On with Azure Active Directory
  • Project-Level RBAC — Granular role-based access control per project
  • CRL Distribution — Automated Certificate Revocation List generation and distribution

Improvements

  • Improved dashboard with real-time status updates
  • Enhanced certificate search and filtering
  • Updated API documentation with OpenAPI 3.0 specifications
  • Improved container image security

Version 1.0.5

Release Date: 2025-03-20

New Features

  • OCSP Responder — Real-time Online Certificate Status Protocol responder
  • Secret Versioning — Full version history with rollback capabilities
  • Agent Management — Centralized agent registration and monitoring
  • LDAP Integration — Directory service authentication support

Version 1.0.4

Release Date: 2024-12-10

New Features

  • Internal Certificate Authority — Full PKI with root and intermediate CA support
  • Certificate Lifecycle Management — Request, approve, issue, renew, and revoke certificates
  • Helm Charts — Standardized Kubernetes deployment via Helm

Version 1.0.3

Release Date: 2024-09-15

New Features

  • Azure Key Vault Integration — Sync secrets with Azure Key Vault
  • Secret Rotation — Automated and manual secret rotation
  • Kubernetes Deployment — AKS deployment with Terraform

Version 1.0.2

Release Date: 2024-06-20

New Features

  • Role-Based Access Control — User roles and permissions
  • Project Management — Organize secrets into projects
  • API v1 — Full REST API for secrets management

Version 1.0.1

Release Date: 2024-03-01

Initial Release

  • Encrypted secrets storage with AES-256-GCM
  • Web-based management interface
  • PostgreSQL backend with Redis caching
  • Docker Compose deployment
  • Local authentication with SRP protocol

Support Policy

MazeVault supports the current version and one previous minor version. Customers on older versions are encouraged to upgrade to receive security updates and new features.