aethercert
Dokumentation
Fleet

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

PartWhat it is
ParametersWhat the installer fills in per deploy target: text, number, yes/no, a list of texts, or a secret.
ConnectionWhere REST and TLS operations connect - a host (optionally a port and "accept a self-signed certificate"), a local socket, or nothing.
AuthenticationHow the agent signs in once for every REST operation: HTTP Basic, an API key, OAuth2 client credentials, or a session login.
OperationsWhat 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 flowThe steps that run, top to bottom. One operation can run in more than one step.
RollbackOptional 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:

RefusedRanges
Loopback127.0.0.0/8, ::1
Link-local, including the cloud metadata address169.254.0.0/16, fe80::/10
Multicast and unspecified224.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.

RootWhat 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, pfxThe 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 writtennoyesis ""
"" (empty text)noyesis ""
"x"yesnois not ""
falseyesnocompared as "false"
0yesnocompared as "0"
empty list []yes (present, but empty)nonot allowed - use "contains"
["a"]yesnonot 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.com becomes _wildcard.example.com, / \ : * ? " < > | are removed, .. collapses and a reserved Windows name such as CON gets a _ prefix - a value can never add a folder or climb out of one.
  • The path must be absolute (/... or C:\...) 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, default 0755, or 0700 for a sensitive file) and the optional dirOwner / 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

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.
  • current is a relative link (releases/<id>), so it resolves the same way inside a container that mounts baseDir somewhere else.
  • The .aethercert marker holds metadata only - no key material. The agent deletes only folders that carry it.
  • Programs always read <baseDir>/current/<file>, never a path inside releases/.

What a write does

  1. 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.
  2. Does nothing when nothing changed. If current already 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.
  3. 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>.
  4. Switches current to the new release in one atomic rename.
  5. Tidies up only after the switch succeeded: releases beyond keep are 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

FieldUsed byWhat it is
actionbothwrite (deploy a new release) or activate (switch current to an existing one). Required.
baseDirbothThe bundle's folder, a path like a file destination (literal text, string parameters, {{ certificate.commonName }}, {{ certificate.shortId }}). Required.
fileswrite1 to 8 files, each { name, content, mode?, sensitive? }. Required.
files[].namewriteA plain file name such as fullchain.pem: letters, digits, ., _, -, no folder, no .., not starting with a dot, unique in the list.
files[].contentwriteWhat the file contains - references like a file operation's content ({{ certificate.fullchainPem }}, {{ chain }}, {{ privateKey }}), no secrets.
files[].modewriteOctal, default 0644, or 0600 for a sensitive file. Never group- or world-writable; a sensitive file grants nothing to others (0640 is fine).
files[].sensitivewriteMarks key material.
keepwritePrevious releases to keep, 1 to 10, default 3.
dirModewriteOctal 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.
dirGroupwriteGroup of baseDir, releases/ and each release folder: a group name, a numeric id, or one parameter.
dirOwnerwriteOwner 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, fileGroupwriteOwner and group of every file in the release, the same way.
releaseIdactivateThe 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 fieldwriteactivate
releaseIdThe release current points to now.The release switched to.
previousReleaseIdThe release current pointed to before, or empty on the first deployment.The release current pointed to before the switch.
certificateFingerprintSha256The deployed certificate's SHA-256 fingerprint.The fingerprint of the release switched to.
baseDirThe resolved release folder.The resolved release folder.
changedfalse 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.pem

nginx 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 container

Do not bind-mount a single file

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's StrictModes. 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 /tmp or a user's home.
  • a symbolic link sits anywhere inside baseDir other than current itself.
  • baseDir or an existing release folder is more open than dirMode. The agent never loosens or tightens an existing folder; change it yourself or pick a new baseDir.
  • baseDir is not covered by a granted filesystem.write entry 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

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 -Compress

What the script boundary does and does not promise

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 .json file. The file is read in your browser; nothing is uploaded until you save.
  • Paste JSON takes a DeploymentTarget manifest (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.

StageWhat it checksWhat you see
ParsingThat 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.
SchemaThe 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.
SemanticThe rules between fields: references to declared parameters and state, executors and their payloads, workflows, conditions.The first problem found, with its path.
SecuritySecrets, 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.

BadgeMeaning
ManualBuilt in the form editor.
ImportedImported from a file or pasted as JSON.
AI generatedYou 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:

  1. 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.
  2. Choose an organization. The assistant works in exactly the organization you choose, and only in that one.
  3. 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:

PermissionWhat it allows
Read the Target RegistryRead the schema and the vocabulary, search and read the targets you can see, and check manifests.
Create draftsAdditionally 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 fleetRead 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 connectedKeep 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 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.

Auf dieser Seite