Target Registry authoring reference
How a registry package is put together - parameters, operations, the deployment flow, conditions, state, files, release bundles, PowerShell and rollback - and how to start one from JSON or with an AI assistant over MCP.
This page is for publishers. It describes the model behind Store > Target Registry >
New Registry Target (schema registry.aethercert.com/v2). If you only install
packages, Target Registry is all you need.
The parts of a package
| Part | What it is |
|---|---|
| Parameters | What the installer fills in per deploy target: text, number, yes/no, a list of texts, or a secret. |
| Connection | Where REST and TLS operations connect - a host (optionally a port and "accept a self-signed certificate"), a local socket, or nothing. |
| Authentication | How the agent signs in once for every REST operation: HTTP Basic, an API key, OAuth2 client credentials, or a session login. |
| Operations | What the target can do. Each has a name, a capability (what it does), and one way of running: REST, file, release bundle, TLS check, service, or PowerShell. |
| Deployment flow | The steps that run, top to bottom. One operation can run in more than one step. |
| Rollback | Optional steps that try to undo a failed deployment. |
Two things are derived for you and never typed: the package's capabilities (the union of its operations' capabilities, a label for people browsing the Store) and its permissions (exactly what the operations need - publishing refuses both less and more).
Category
Category (metadata.family) decides where the package is listed in the Store and in
every deploy-target picker: Windows roles, Web servers, Load balancers, Firewalls,
Hypervisors, DevOps or Custom scripts (windows_role, webserver, load_balancer,
firewall, hypervisor, devops, script). It is optional. Releasing a version that
sets it moves the package, and the deploy targets created from it, to that category; a
version without it keeps the category the package already has. Any other value is refused.
Parameter rules
A text, list or secret parameter can carry a pattern (a regular expression every
value - each entry of a list - must match) and a max length in characters. The
dashboard refuses to save a deploy target whose values break them, and the agent checks
them again before the first step runs. Write patterns the agent can run: anchored with
^ and $, no lookahead/lookbehind and no backreferences.
A parameter used as the connection host, or as a TLS check's host, must hold only a host
name or IP address. The dashboard and the agent refuse a value with a port
(linux-01.example.com:8443), a scheme (https://...) or a path - the port belongs in
the port parameter. IPv6 addresses work with or without brackets. Give such a parameter a
hostname pattern so the form explains the mistake early, for example:
{ "key": "verifyHost", "label": "Verify via TLS on host", "type": "string",
"pattern": "^([A-Za-z0-9]([A-Za-z0-9.-]{0,251}[A-Za-z0-9])?|[0-9A-Fa-f]*:[0-9A-Fa-f]*:[0-9A-Fa-f:.]*)$",
"maxLength": 253 }Category, pattern and max length need agent 2026.10.10.0.0 or newer. An older agent is not sent a version that uses them; its deployment waits until the agent has updated.
Which addresses the agent connects to
REST operations and TLS checks connect from the agent's host. The agent resolves the host name itself and refuses these addresses, whatever the permissions say:
| Refused | Ranges |
|---|---|
| Loopback | 127.0.0.0/8, ::1 |
| Link-local, including the cloud metadata address | 169.254.0.0/16, fe80::/10 |
| Multicast and unspecified | 224.0.0.0/4, ff00::/8, 0.0.0.0, :: |
Private ranges are allowed, because on-premises targets live there: 10.0.0.0/8,
172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 and fc00::/7. A TLS check of the
server the agent runs on must therefore use a name or address that resolves to its LAN or
public address, never localhost or 127.0.0.1.
References
Values reach an operation through references written as {{ root.path }}. There are no
expressions or functions - a reference is replaced by its value, nothing more.
| Root | What it holds |
|---|---|
config.<key> | A parameter the installer filled in. |
secret.<key> | A secret parameter. Only usable in authentication, REST headers, bodies and query values, and secret script parameters - never in a path, a file, a condition or an output. |
state.<key> | A value an earlier step wrote (see State). |
certificate, chain, privateKey, pfx | The key material being deployed (PEM, .base64, .derBase64; pfx and pfx.password). Using one gives the operation that material - and nothing else. |
job.* | Facts about the job: id, commonName, domains, isRenewal. |
A secret never appears in the package itself - only the reference does. The installer's value is stored encrypted and redacted from every error.
State
An operation can declare outputs: values it picks from a REST response ($.id,
$.data.name, or $status for the HTTP status code) or from the JSON object a PowerShell
operation prints on its last line. Each output is written to state.<name> after the step
succeeds, and later steps read it - in a path, a body, a condition or a PowerShell
parameter.
A step can only read state from steps above it; the editor offers nothing else and publishing checks it. A state value that was never written - because the step that writes it was skipped - is empty.
Conditions
A step can run only when a condition holds: one value, one comparison. A step whose condition is false is shown as skipped and the flow continues.
| Value | "has a value" | "is empty or not set" | "is" "" / "is not" "" |
|---|---|---|---|
| not set / never written | no | yes | is "" |
"" (empty text) | no | yes | is "" |
"x" | yes | no | is not "" |
false | yes | no | compared as "false" |
0 | yes | no | compared as "0" |
empty list [] | yes (present, but empty) | no | not allowed - use "contains" |
["a"] | yes | no | not allowed - use "contains" |
- "is on" / "is off" exist only for yes/no parameters; a value that is not set counts as off.
- "contains" exists only for lists, and is false on an empty or unset list.
- Recommendation: for an optional text where the content matters (a chain path, a service name, a previous thumbprint), use "is not" with an empty value rather than "has a value". It says exactly what you mean.
Files
A file operation writes the certificate, chain or key to a full destination path on the agent's host - on Linux and Windows alike. Prefixes and suffixes are part of the path:
"file": {
"destination": "/etc/ssl/private/ae-{{ certificate.commonName }}/privkey.pem",
"content": "{{ privateKey }}",
"sensitive": true,
"dirMode": "0700"
}- What a path may contain: literal text, string parameters (
{{ config.keyPath }}- the installer's value is used as path text, and a parameter's default may itself contain the two path variables below), and the path variables{{ certificate.commonName }}(the common name, else the first DNS name) and{{ certificate.shortId }}(a short, stable id of the certificate). Nothing else - no secrets, state or other certificate fields. The path variables are not key material: they need no material and no certificate read permission. - Path variables are made safe for a path. A wildcard
*.example.combecomes_wildcard.example.com,/ \ : * ? " < > |are removed,..collapses and a reserved Windows name such asCONgets a_prefix - a value can never add a folder or climb out of one. - The path must be absolute (
/...orC:\...) and contain no..segment. The agent also refuses a path that runs through a symbolic link or junction. - Missing folders are created, at any depth. Only folders the agent creates get
dirMode(an octal string, default0755, or0700for a sensitive file) and the optionaldirOwner/dirGroup(a user or group name, or one parameter). Existing folders are never changed: if a sensitive file's own folder already exists and is more open than requested, the deployment fails with an error instead. On Windows the three fields are ignored; a sensitive file's new folders are readable only by the agent, SYSTEM and Administrators, without inherited permissions. - Writes are atomic and repeatable. Each file is written to a temporary file in its folder and renamed into place. Running the same deployment again changes nothing. If one of several file steps in a row fails, the files written before it are restored, so a failed write never leaves a new key next to an old certificate.
The derived permission permissions.filesystem.write lists each destination. An entry also
covers everything below it - C:\ProgramData\aethercert covers
C:\ProgramData\aethercert\ae-{{ certificate.shortId }}\cert.pem - and publishing refuses a
destination no entry covers, and an entry that covers no destination.
Agent version
Literal paths, the path variables, folder entries and dirMode / dirOwner / dirGroup
need agent 2026.10.9.0.0 or newer. An older agent is not sent such a package; the event log
says which version it needs.
Release bundles
A bundle operation (executor bundle) writes the certificate, chain and key as one
release: every deployment gets its own folder, and a symbolic link named current is
switched to it in a single step. A program reading current/ sees either the complete
old release or the complete new one - never a new key next to an old certificate - and the
previous release stays on disk, so a failed deployment can switch straight back to it.
Use it for anything that reads certificate files from Linux or macOS: nginx, Apache, HAProxy, Postfix, a container with a bind mount, or a plain file target.
Layout
/etc/aethercert/ae-example.com/ baseDir
├── current -> releases/20261023T101500Z-3f9a1c
└── releases/
├── 20261023T101500Z-3f9a1c/ the active release
│ ├── .aethercert marker: release id, previous release, fingerprint
│ ├── fullchain.pem
│ ├── chain.pem
│ └── privkey.pem
└── 20260824T091200Z-c41e07/ the previous release- A release id is the UTC time of the deployment and the first six hex digits of the
certificate's serial number, for example
20261023T101500Z-3f9a1c. currentis a relative link (releases/<id>), so it resolves the same way inside a container that mountsbaseDirsomewhere else.- The
.aethercertmarker holds metadata only - no key material. The agent deletes only folders that carry it. - Programs always read
<baseDir>/current/<file>, never a path insidereleases/.
What a write does
- Checks the material first. The certificate and chain must be valid PEM, the leaf must come first in every file that contains it, and a private key must belong to the certificate. If not, the deployment fails before anything is written.
- Does nothing when nothing changed. If
currentalready holds a release of the same certificate with exactly the same files, the step succeeds without writing and returns that release's id. Running the same deployment twice changes nothing on disk. - Writes the new release next to the old ones, in a private temporary folder. Each
file is created new (never through an existing file or link), gets its mode and owner,
and is flushed to disk. The finished folder is then renamed to
releases/<id>. - Switches
currentto the new release in one atomic rename. - Tidies up only after the switch succeeded: releases beyond
keepare deleted, oldest first - never the active release and never the one directly before it. Leftover temporary folders from an interrupted run are removed once they are older than an hour.
If anything fails before step 4 - including the agent stopping halfway - current still
points to the previous release, untouched.
Fields
| Field | Used by | What it is |
|---|---|---|
action | both | write (deploy a new release) or activate (switch current to an existing one). Required. |
baseDir | both | The bundle's folder, a path like a file destination (literal text, string parameters, {{ certificate.commonName }}, {{ certificate.shortId }}). Required. |
files | write | 1 to 8 files, each { name, content, mode?, sensitive? }. Required. |
files[].name | write | A plain file name such as fullchain.pem: letters, digits, ., _, -, no folder, no .., not starting with a dot, unique in the list. |
files[].content | write | What the file contains - references like a file operation's content ({{ certificate.fullchainPem }}, {{ chain }}, {{ privateKey }}), no secrets. |
files[].mode | write | Octal, default 0644, or 0600 for a sensitive file. Never group- or world-writable; a sensitive file grants nothing to others (0640 is fine). |
files[].sensitive | write | Marks key material. |
keep | write | Previous releases to keep, 1 to 10, default 3. |
dirMode | write | Octal mode of the bundle's folders, default 0700. Must give the owner rwx and never grant group or world write - 0750 lets a group read the files. |
dirGroup | write | Group of baseDir, releases/ and each release folder: a group name, a numeric id, or one parameter. |
dirOwner | write | Owner of each release folder, the same way. baseDir and releases/ always stay owned by the agent's own user, so nobody else can switch current. |
fileOwner, fileGroup | write | Owner and group of every file in the release, the same way. |
releaseId | activate | The release to switch to - usually {{ state.previousReleaseId }}. Required. |
A write needs the certificate material: the release id and fingerprint come from the
certificate. baseDir must be covered by permissions.filesystem.write, exactly like a
file destination; the editor derives the entry for you.
Outputs
| Result field | write | activate |
|---|---|---|
releaseId | The release current points to now. | The release switched to. |
previousReleaseId | The release current pointed to before, or empty on the first deployment. | The release current pointed to before the switch. |
certificateFingerprintSha256 | The deployed certificate's SHA-256 fingerprint. | The fingerprint of the release switched to. |
baseDir | The resolved release folder. | The resolved release folder. |
changed | false when nothing was written because the release was already current. | false when current already pointed to it. |
Map the ones you need to state with outputs, for example
"outputs": { "releaseId": "$.releaseId", "previousReleaseId": "$.previousReleaseId" }.
Example: nginx with rollback
"operations": {
"writeRelease": {
"capability": "certificate.importWithPrivateKey",
"executor": "bundle",
"materials": ["certificate", "chain", "privateKey"],
"bundle": {
"action": "write",
"baseDir": "/etc/aethercert/ae-{{ certificate.commonName }}",
"files": [
{ "name": "fullchain.pem", "content": "{{ certificate.fullchainPem }}" },
{ "name": "chain.pem", "content": "{{ chain }}" },
{ "name": "privkey.pem", "content": "{{ privateKey }}", "sensitive": true }
],
"keep": 3
},
"outputs": { "releaseId": "$.releaseId", "previousReleaseId": "$.previousReleaseId" }
},
"activatePrevious": {
"capability": "configuration.activate",
"executor": "bundle",
"bundle": {
"action": "activate",
"baseDir": "/etc/aethercert/ae-{{ certificate.commonName }}",
"releaseId": "{{ state.previousReleaseId }}"
}
},
"reloadNginx": {
"capability": "service.reload",
"executor": "service",
"service": { "name": "{{ config.serviceName }}", "action": "reload" }
},
"verifyServed": {
"capability": "deployment.verify",
"executor": "tls",
"materials": ["certificate"],
"tls": {}
}
},
"workflows": {
"deploy": {
"steps": [
{ "operation": "writeRelease" },
{ "operation": "reloadNginx" },
{ "operation": "verifyServed" }
]
},
"rollback": {
"steps": [
{ "operation": "activatePrevious", "when": { "ref": "state.previousReleaseId", "op": "exists" } },
{ "id": "reloadAfterRollback", "operation": "reloadNginx" }
]
}
}The derived permissions are certificate.read and certificate.privateKey,
filesystem.write: ["/etc/aethercert/ae-{{ certificate.commonName }}"], local.service,
and the TLS check's host under network.
If the reload or the TLS check fails, the rollback switches current back to the previous
release and reloads nginx again. The failed release stays in releases/ for diagnosis
until keep removes it. On the very first deployment there is no previous release:
state.previousReleaseId is empty, the guard skips the switch, and an activate without
a release id would fail - so always guard it.
Pointing programs at current
ssl_certificate /etc/aethercert/ae-example.com/current/fullchain.pem;
ssl_certificate_key /etc/aethercert/ae-example.com/current/privkey.pem;
ssl_trusted_certificate /etc/aethercert/ae-example.com/current/chain.pem;SSLCertificateFile /etc/aethercert/ae-example.com/current/fullchain.pem
SSLCertificateKeyFile /etc/aethercert/ae-example.com/current/privkey.pemnginx and Apache read the files when they start or reload, so a reload step after the write picks up the new release.
Docker: mount the whole baseDir, read-only, and point the program inside the
container at current:
docker run -d --name web \
-v /etc/aethercert/ae-example.com:/certs:ro \
nginx
# nginx.conf inside the container:
# ssl_certificate /certs/current/fullchain.pem;
# ssl_certificate_key /certs/current/privkey.pem;
docker kill -s HUP web # after each deployment: reload, no need to re-create the containerDo not bind-mount a single file
-v /etc/aethercert/ae-example.com/current/fullchain.pem:/certs/fullchain.pem is not
supported. Docker resolves the link once and mounts that one file: the container keeps
the old release's file for as long as it runs, and a later deployment never reaches it.
Always mount the folder.
Safety rules
The agent refuses to write - with an error naming the folder - when:
baseDir, or any existing folder above it, is not owned by root (or the agent's own user) or is group- or world-writable, like OpenSSH'sStrictModes. Anyone who could write there could swap a folder for a link while the agent works. Use a dedicated folder such as/etc/aethercert/..., never/tmpor a user's home.- a symbolic link sits anywhere inside
baseDirother thancurrentitself. baseDiror an existing release folder is more open thandirMode. The agent never loosens or tightens an existing folder; change it yourself or pick a newbaseDir.baseDiris not covered by a grantedfilesystem.writeentry after the parameters and path variables are filled in.
Logs and deployment results show the release id, the fingerprint and paths - never file contents.
Linux and macOS only
Release bundles need agent 2026.10.11.0.0 or newer on Linux or macOS. On Windows the step fails with "bundle executor is not supported on Windows yet"; Windows targets usually use the certificate store instead.
PowerShell scripts
Every PowerShell operation is a script your package defines - community publishers
included. Declare each value the script reads as a parameter (type, required, allowed
values, pattern, length) and pass values with with: each value is a literal or exactly
one reference, checked against the parameter before the script starts. Nothing is
ever pasted into script source. The operation needs the local.powershell: script
permission, which the editor derives for you.
Your script runs wherever the installing organization's registry policy allows your package. Community packages are off by default in every organization, so a community package reaches organizations that chose to enable community targets.
A script reads its parameters as $env:AC_PARAM_<name>. With the certificate as material
it also gets $env:AC_CERT_THUMBPRINT, $env:AC_CERT_COMMON_NAME and $env:AC_CERT_SANS;
with certificate and private key, $env:AC_PFX_PATH and $env:AC_PFX_PASSWORD - a
temporary PFX file with restricted access that is deleted afterwards, even when the script
fails. Print outputs as one JSON object on the last line:
$ErrorActionPreference = 'Stop'
Set-AdfsSslCertificate -Thumbprint $env:AC_PARAM_thumbprint
@{ thumbprint = $env:AC_PARAM_thumbprint } | ConvertTo-Json -CompressWhat the script boundary does and does not promise
The script arrives on standard input, never on the command line, and its source contains no secrets. Values in environment variables are still readable inside the process and by sufficiently privileged processes on the host (SYSTEM, administrators, debuggers). The host is trusted - this narrows what a package can touch, it does not hide anything from the machine's own administrators.
Rollback
Define a rollback when the target has a reliable previous state to return to - for
example the certificate an IIS binding used before, which iis.setBindingCertificate
returns as previousThumbprint.
- It runs only when the deployment fails after at least one step succeeded.
- Its steps run in order; a failing one does not stop the others.
- It can read every state value the deployment wrote. Guard each step - e.g. run the
restore only if
state.previousThumbprint"is not"""- so it does nothing when there was nothing to restore. - It is best effort, not a transaction. Nothing is guaranteed to come back, and the
deployment's own error always stays the job's result. The rollback outcome
(
succeeded,partial,failed) is logged next to it. - Order the deployment so destructive steps come last - remove the old certificate only after the new one is bound and verified, so a rollback still has something to bind.
A release bundle always has a reliable previous state: the release
current pointed to before. Its rollback switches back to it (see the example there).
Leave the rollback empty when there is no reliable previous state; a rollback that guesses is worse than none.
Capabilities
Pick a capability from the catalogue (certificate, private key, binding, configuration,
service, deployment) or name your own, such as windowsRole.certificateBind. The
catalogue's own namespaces only accept catalogue names, to catch typos. A capability is a
label: what an operation may do is decided by how it runs, the material it declares and
its permissions - never by its capability name.
Starting from JSON
New Registry Target offers three ways to start:
- Create manually opens the form editor described above.
- Import file opens a manifest
.jsonfile. The file is read in your browser; nothing is uploaded until you save. - Paste JSON takes a
DeploymentTargetmanifest (registry.aethercert.com/v2) you copied from somewhere else - for example one an AI assistant wrote, or one you exported from target discovery.
Imported and pasted manifests open in the JSON editor. A manifest can be up to 256 KiB. The check runs on the server as you type, and Save draft keeps your work even while the manifest still has errors. Once the manifest passes the schema check, Continue in form opens it in the form editor, and Edit as JSON goes back. Releasing always happens from the form's Review & publish step, with the same checks as every other target.
What the check stages mean
The check runs four stages in order. A stage runs only when the stages before it passed; otherwise it is shown as skipped, so one mistake is not reported twice.
| Stage | What it checks | What you see |
|---|---|---|
| Parsing | That the text is one valid JSON object, within the size limit. | The line and column of a syntax error. A key that appears twice in one object is a warning: JSON keeps only the last value. |
| Schema | The structure: required fields, unknown fields, types, allowed values. | Every structural problem at once, each with the path of the field, for example operations.bind.rest.method. |
| Semantic | The rules between fields: references to declared parameters and state, executors and their payloads, workflows, conditions. | The first problem found, with its path. |
| Security | Secrets, key material, permissions, trust levels, literal hosts and paths, PowerShell scripts. | The first problem found, with its path. |
The semantic and security stages stop at the first problem, exactly like the agent does.
When one of them fails, the other is shown as skipped; fix the problem and check again.
Each issue carries a code, such as SCHEMA_UNKNOWN_FIELD or PERMISSION_MISSING, that
names the kind of problem.
One rule is softer while you work: a permission that no operation needs is a warning in a draft. It is still refused when you release, because a package may declare exactly the permissions its operations need - no less and no more.
Next to the issues, the security summary lists what the target would be allowed to do: how it runs, which hosts it connects to, whether it receives the private key, which files it writes, whether it controls a service or runs PowerShell scripts, and any text that looks like a hard-coded password or token. The summary is informational: it helps whoever releases the target, and it never decides whether the manifest is valid. It appears once the manifest passes the semantic and security stages.
Draft provenance
Every draft shows where it came from, in the In progress list and in the editor header, for example Source: AI generated · Status: Draft.
| Badge | Meaning |
|---|---|
| Manual | Built in the form editor. |
| Imported | Imported from a file or pasted as JSON. |
| AI generated | You declared that an AI wrote it, or an AI assistant created it over MCP. |
aethercert sets the source when the draft is first saved, and it never changes after that. Editing an AI-generated draft in the form keeps it AI-generated. When you paste or import a manifest, tick This manifest was generated by an AI if that is the case, and optionally name the AI provider. You can mark a draft as AI-generated, but nothing can mark an AI-generated or imported draft as manual.
Imported and AI-generated drafts show a notice in the review step. Before you release one, check what each operation does and what it may access - the security summary lists it. The released version keeps a record of how it was authored and who released it.
Connecting an AI assistant over MCP
An AI assistant that supports the Model Context Protocol (MCP) can work with the Target Registry for you: read the schema and the vocabulary, look at existing targets as examples, check a manifest it wrote, and save it as a new draft for you to review.
Endpoint: https://api.aethercert.com/api/mcp
Add this URL as a remote MCP server in your assistant. The assistant must support MCP authorization with OAuth and Client ID Metadata Documents; an assistant that only supports dynamic client registration, or that runs as a web page in your browser, cannot connect.
Allowing the connection
The first time the assistant connects, it opens the aethercert dashboard in your browser:
- Sign in with your second factor (or a passkey). If you normally use an MSP portal domain, sign in on the dashboard address the assistant opens.
- Choose an organization. The assistant works in exactly the organization you choose, and only in that one.
- Review and allow. The consent page shows the assistant's name - which is the assistant's own claim - and the domain that identifies it. aethercert does not verify who runs that domain, so allow it only if you trust it. Then choose Allow or Deny.
The assistant can ask for these permissions:
| Permission | What it allows |
|---|---|
| Read the Target Registry | Read the schema and the vocabulary, search and read the targets you can see, and check manifests. |
| Create drafts | Additionally save a manifest as a new draft in the chosen organization, marked AI-generated. Needs the admin or owner role there. If you do not have it, only read access is granted. |
| Read your fleet | Read your agents, certificates, deploy targets and deployments, including each deployment step's status, duration and error - to help you debug a deployment. Changes nothing. |
| Stay connected | Keep working without asking you again, for up to 30 days. |
When the assistant later needs a permission it does not have yet - for example creating drafts after it could only read - it asks again, and the consent page shows the permissions the connection already holds next to the new ones. Allowing extends the existing connection: the assistant keeps one entry in Connected apps with all its permissions, and its previous access ends.
An AI assistant can never release a target
The assistant can only create new drafts. It cannot change or discard an existing draft, and it can never release, publish, install or configure a target. Every draft it creates is marked AI generated and waits in In progress until a person in your organization reviews it and releases it - or discards it. Treat its manifest like code from an unknown author: check the operations and permissions before you release.
The assistant gets a link to each draft it creates. A draft it creates for a package that already has a draft is refused, so it never overwrites work in progress. Your organization can receive at most 20 drafts per hour this way.
How long a connection lasts
A connection ends by itself after 7 days without use, and after 30 days at the latest. The assistant then asks you to allow it again. If you are removed from the organization, the connection stops working at once.
Revoking a connection
Settings > Connected apps lists every assistant you connected, with its organization, its permissions and when it was last used. Revoke ends a connection immediately: the assistant's next request is refused.
Signing out everywhere, signing out your other devices, and resetting your password also disconnect every connected assistant.
Target Registry
Community, MSP and aethercert-published deploy target templates you install rather than configure by hand - trust levels, org policy, and what publishing one means.
Organizations
The tenant boundary in aethercert: creating one, switching between them, the organization settings pages, billing, and deleting one.