Register targets and secrets¶
A target is one system MEHO operates against — a Kubernetes API
server, a vCenter, a Vault, a firewall. Registration gives the
backplane three things: the coordinates to reach the system (host,
port, TLS trust), the product token that selects which connector
serves it, and a secret_ref — a reference into your credential
store. MEHO never stores the credential itself; it resolves the
reference at dispatch time, under an identity your store can audit.
Until at least one target is registered and its probe is green, an agent session can discover operations but cannot act on anything — which is why this is the first guide.
Prerequisites and roles
- A running backplane (Install & operate)
and an authenticated CLI session
(Connect clients):
meho login https://meho.example.com. - Registration verbs (
import, create, edit, delete) require the tenant_admin role. Read verbs (list,describe,probe,discover) need operator. If a step below answers403 insufficient_role, that is your JWT's role, not a broken deploy.
Choose a registration path¶
| Path | What it is | When to use it |
|---|---|---|
CLI — meho targets import |
Bulk-import from a targets.yaml file you keep in version control. |
The canonical path. Your target registry becomes a reviewable, re-runnable file. |
Operator console — /ui/connectors |
Create/edit forms plus a paste-or-upload targets.yaml import with a preview table. |
Console-first operators. Produces byte-identical writes to the CLI import. |
REST — POST /api/v1/targets |
The API the other two paths call. | Automation that already speaks REST. |
There are no MCP tools for registering targets or staging secrets —
deliberately. Registration is an operator-trust decision, so it
lives on the operator surfaces above; agents get the read-only
list_targets tool and consume whatever you registered. This is also
why the CLI is worth installing before your first agent session. If
that boundary ever moves, the change will be tracked on the
feature-maturity index.
Write a targets.yaml¶
The file is a targets: list. A minimal, realistic pair — a
Kubernetes cluster and a VMware appliance that is reached by IP:
targets:
- name: lab-rke2
product: k8s # registered product token — NOT "kubernetes"
host: rke2-api.lab.example.com
port: 6443
auth_model: shared_service_account
notes: RKE2 lab cluster, evaluation estate.
- name: lab-vcenter
product: vmware
host: 10.20.0.15 # reached by IP...
tls_server_name: vc01.lab.example.com # ...but the cert's SAN is the FQDN
tls_ca_pin: |
-----BEGIN CERTIFICATE-----
...internal CA, PEM...
-----END CERTIFICATE-----
notes: vCenter reached by IP; SNI + cert verification pinned to the SAN name.
Mapping rules the importer applies:
- Recognised top-level keys map 1:1 to real columns:
name,aliases,product,host,port,fqdn,secret_ref,auth_model,vpn_required,notes,preferred_impl_id,verify_tls,tls_ca_pin,tls_server_name,secret_field_map,extras. name,product, andhostare required and validated locally before any HTTP request.- Unknown keys are not errors — they spill into the target's
extrasJSON column, so you can carry your own annotations. fingerprintis server-managed and dropped with a warning; the probe verb is its only writer.verify_tls: falseandtls_ca_pinare mutually exclusive (422): pin the CA or disable verification, never both.- Set
tls_server_namewhenever you reach an appliance by IP (or by an alias) while its certificate only carries the FQDN — without it, dispatch fails withconnector_tls_verify_failed, carrying Python's own "IP address mismatch" text insideextras.exception_message.
You can leave secret_ref out entirely — see the next section.
Use a secret with different field names¶
A connector reads fixed field names from the secret. For example, SSH
reads ssh_private_key and username. If your secret already holds the
right values under other names, you do not need a copy. Tell MEHO which
field to read instead with secret_field_map:
targets:
- name: web-01
product: linux
host: web-01.example.com
secret_field_map:
ssh_private_key: private_key # the connector field: the field in your secret
username: login
On the CLI, use --secret-field ssh_private_key=private_key with
meho targets add, meho targets update or meho targets import.
meho targets probe then checks that every field the map points to
exists in the secret. It names any missing field, never a value. For SSH,
a field named private_key also works with no map at all.
Stage the secret¶
Every target that needs credentials carries a secret_ref. Which
store it points at is decided by your deploy's credential backend
(vault, the default, or gsm — see the
install trail).
Vault (default backend)¶
The secret_ref is the logical KV-v2 path relative to the secret
mount — never include the mount or the /data/ segment. The
canonical, enforced layout is per-tenant:
tenants/<tenant-id>/<target-name>
If you omit secret_ref at registration, the server derives exactly
that path for you. The reliable recipe is therefore: register first,
read the derived reference back, then stage the secret at it.
# 1. After import, read the derived reference:
meho targets describe lab-vcenter
# ...
# secret_ref: tenants/4f6f6f6f-.../lab-vcenter
# 2. Stage the credential there with your own Vault tooling.
# The Vault CLI addresses the *wire* path, which prepends the mount:
vault kv put secret/tenants/4f6f6f6f-.../lab-vcenter \
username=svc-meho password='...'
The two path notations trip everyone once, so:
| Context | Path shape | Example |
|---|---|---|
MEHO secret_ref |
logical, no mount | tenants/<tenant-id>/lab-vcenter |
| Vault CLI / API | mount-prefixed | secret/tenants/<tenant-id>/lab-vcenter |
Rules the backplane enforces:
- An explicitly supplied Vault
secret_refmust stay inside your tenant subtree (tenants/<tenant-id>/…). Anything else is rejected at registration with a structured 422 (secret_ref_outside_tenant_scope) that names the expected path — because your operator Vault identity could never read it at dispatch anyway. - A mount- or API-shaped ref (
secret/data/…,kv/data/…,data/…) is rejected at read time: Vault inserts the/data/segment itself, so a prefixed value double-resolves to a 404. - Most connectors expect the fields
usernameandpassword. Some want a different shape — the Kubernetes connector reads a single field namedkubeconfigholding the kubeconfig YAML. The per-connector field contract is listed in each connector's onboarding notes (docs/cross-repo/).
# Kubernetes example — one field, whole kubeconfig as the value:
vault kv put secret/tenants/<tenant-id>/lab-rke2 \
kubeconfig=@rke2-kubeconfig.yaml
Google Secret Manager (credentialBackend: gsm)¶
The ref grammar is:
gsm:<project-id>/<secret-name>[/versions/<version>][#<field>]
gsm:my-project/lab-vcenter-creds— latest version, whole payload.gsm:my-project/lab-vcenter-creds/versions/3— pinned version.gsm:my-project/lab-vcenter-creds#password— one field only.
The secret's payload must be a JSON object of named fields, e.g.
{"username": "svc-meho", "password": "..."} — the same field names
the connector expects on Vault. A kubeconfig ref looks like
gsm:my-project/lab-rke2-kubeconfig#kubeconfig.
On a GSM deploy, set secret_ref explicitly in the YAML (the
server-derived default is the Vault layout).
MEHO reads a gsm: secret only from a Google Cloud project listed in
the setting GSM_PARAM_ALLOWED_PROJECTS (comma-separated project ids,
set through the chart's extraEnv). The list is empty by default, so
list your projects before you register targets. A target whose
secret_ref names a project that is not listed is refused at
registration (422 secret_ref_project_not_allowed), and a stored
target in such a project fails every dispatch with
connector_error: GcpSecretManagerReadError until the project is
listed. The same list applies to gsm: refs in operation parameters.
Targets that need no credential¶
Some products need no login, for example a Prometheus that you reach
through a port-forward. If you leave secret_ref out, the server still
derives the default path, and every call then fails on the Vault read.
To store the target with no credential, send an explicit null:
# Create it with no credential, over plain HTTP:
meho targets add lab-prom --product prometheus --host prom.example.com \
--port 9090 --no-secret-ref --extra scheme=http
# Or fix a target that already exists:
meho targets update lab-prom --clear secret-ref --extra scheme=http
--extra on update replaces the whole extras map, so pass every key
you want to keep. --clear extra removes every extra, and --clear alias
removes every alias. In YAML, write secret_ref: null.
Change a secret¶
MEHO keeps a login for each target for a while, so it does not have to log in on every call. When you change a secret only in Vault (or your secret store), MEHO keeps using the old login until it expires.
To make MEHO read the secret again right away, send the same
secret_ref again:
meho targets update lab-vcenter --secret-ref tenants/<tenant-id>/lab-vcenter
Any change to how MEHO reaches or logs in to a target (for example
--host, --secret-ref, or --extra) does the same: the next call
logs in with the new values.
The MEHO server that gets the update drops its login at once. Any other backplane server, and any satellite runner, drops its login on its next call to that target.
Import and verify¶
# 1. Preview — classifies every entry CREATE / UPDATE, writes nothing:
meho targets import targets.yaml --dry-run
# 2. Apply. Default mode aborts before any write if a name already exists:
meho targets import targets.yaml
# Applied: 2 created, 0 updated.
# 3. Iterate later with --update: PATCHes existing names, creates new ones.
# The PATCH is sparse — fields your YAML omits are left untouched.
meho targets import targets.yaml --update
Then probe. The probe invokes the connector matched to the target's
product, live, and persists the resulting fingerprint:
meho targets probe lab-rke2
vendor: kubernetes
product: rke2
version: v1.31.4+rke2r1
reachable: true
probed_at: 2026-07-31T09:14:22Z
probe_method: GET /version
Probe-green means reachable: true with the product/version
identified. (The fingerprint's product is what the connector
observed — here the RKE2 distribution — which can be more specific
than the product token you registered the target under.) The fingerprint is cached on the target row —
meho targets describe lab-rke2 shows it from then on without
re-probing, and the connector resolver uses it to pick the right
implementation version at dispatch time.
Two useful companions:
meho targets list --product k8s # everything registered, filterable
meho targets discover k8s # candidate systems a connector can see
# from an already-registered seed target
What can go wrong here¶
Real failure modes, with the errors they actually produce:
| Symptom | What it means | Fix |
|---|---|---|
| 422 at registration: "target destination is not a public address; refusing it as a server-side request forgery risk…" | The SSRF guard rejects private/internal addresses by default. The number-one surprise on on-prem first installs. | Add your internal ranges or hostnames to the chart's config.targetSsrfAllowlist (env MEHO_TARGET_SSRF_ALLOWLIST) and upgrade the release. |
422 unknown_product, listing valid_products |
The product token isn't one a registered connector advertises. Product tokens are exact — k8s, not kubernetes. |
Pick a token from the error's valid_products list. |
422 secret_ref_outside_tenant_scope |
You supplied a Vault ref outside tenants/<tenant-id>/…. It would fail with "permission denied" at every dispatch. |
Use the expected_secret_ref the error names — or omit secret_ref and take the server default. |
422 secret_ref_project_not_allowed |
You supplied a gsm: ref in a Google Cloud project that GSM_PARAM_ALLOWED_PROJECTS does not list. MEHO would refuse every read of it. |
List the project in GSM_PARAM_ALLOWED_PROJECTS and upgrade the release — or point secret_ref at a secret in a listed project. |
Dispatch fails, connector_error: GcpSecretManagerReadError — "…in the Google Cloud project '…', which GSM_PARAM_ALLOWED_PROJECTS does not list…" |
The target's gsm: login names a project that is not on the list (for example a target registered before the list was set). Nothing was read from Google. |
List the project in GSM_PARAM_ALLOWED_PROJECTS and upgrade the release. |
| Probe exits with 501 — "no connector registered for product=…" | The target row is fine; no connector serves that product on this backplane version. Any cached fingerprint is left untouched. | Check the connector catalog for your version; for spec-ingested (generic) connectors, ingest one first. |
Dispatch fails, connector_error: VaultCredentialsReadError — "…has a KV-v2 API-path-shaped secret_ref…" |
Your ref embeds the mount or a data/ segment (secret/data/…). |
Store the logical path only: tenants/<tenant-id>/<name>. |
Dispatch fails, connector_error: VaultCredentialsReadError — "…is missing required field '…'" |
The secret exists but doesn't carry the field the connector needs (username/password, or kubeconfig for Kubernetes). |
Re-stage the secret with the connector's field names. |
Dispatch fails, connector_vault_forbidden — Vault answered permission denied |
Nine times out of ten the secret is staged at the wrong path, not a missing grant: the error reads like a policy problem but the ref simply doesn't match where you wrote the secret. The error names the expected path. | vault kv put the credential at the expected path. Do not widen the backplane's Vault policy — it is deploy-owned and re-applied on upgrade. |
Dispatch fails, connector_auth_failed (HTTP 401 at session establish) |
The credential resolved but the vendor rejected it — usually a password rotated upstream while the store copy lagged. | Re-stage the current credential at the same path, then retry. |
Dispatch fails TLS: extras.error_code is connector_tls_verify_failed and extras.exception_message contains "IP address mismatch" (CPython ssl text relayed verbatim, not a MEHO string — MEHO's own summary says the chain "is not trusted") |
You reach the appliance by IP but its certificate only names the FQDN. | Set tls_server_name: <cert-fqdn> on the target and re-import with --update. |
Probe shows reachable: false / auth_failed on a GSM deploy with per-operator Workload Identity Federation |
Probes run under a system placeholder identity that cannot complete the per-operator token exchange, so credentialed-target probes cannot read the secret on that configuration. Reachability of uncredentialed endpoints still probes fine. | Known limitation — documented in docs/deploying.md; verify credentialed dispatch with a real operation call instead. |
Registration accepted, but every dispatch fails with a no_connector error naming the wrong thing |
A product no connector implementation actually serves can slip through registration in some cases; the dispatch-time error then misattributes the cause. |
Known issue, tracked in evoila/meho#2701. Re-check the target's product against the connector catalog. |
Prove the secret is readable — without printing it
If Vault itself is registered as a MEHO target, dispatch the
metadata-only vault.kv.versions operation against the
credential's path instead of a read: it proves the path resolves
and the policy allows access while returning version numbers,
never values.
To check that copies of one secret still match (for example after
a password change), use vault.kv.compare for two or more Vault
paths, or k8s.secret.compare_to_ref for a Kubernetes Secret key
and its Vault copy. They answer only "same" or "different" per
copy, never a value or a hash.
Next: Run your first operations.