Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / src / push / pu-private.h
Author[]Andy Green <andy@warmcat.com> 2026-09-18 19:10 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-09-21 07:06 UTC
Tree461583a5032f16cb9664fcf429dedda9d5c87567   Raw Patch
 
power: authenticate builder registration with the link secret, fixes F-022
power: authenticate builder registration with the link secret, fixes F-022

sai-power's LAN-facing registration endpoint accepted
com.warmcat.sai.builder_registration from anyone who could reach its
listen port: unknown power_controller_name values dynamically created
PCONs, and the power_on/off/monitor URLs from the message were copied
into the PCON and became ss client targets fetched from the
sai-power host.  A LAN peer could thus register itself onto someone
else's PCON, retarget builder->PCON mappings, and point power events at
attacker-chosen or internal URLs (SSRF from the power host's network
position, plus a tracking beacon).

Builders now prove the fleet-wide link secret in the registration
message ("secret" member, the same link-key conf sai-server
authenticates builder connections with), compared in constant time
before any PCON mutation or persistence; sai-power fails closed when
no link-key is configured.  The secret is deliberately not part of the
sticky-registration sqlite map, so it is not written at rest.

Two residuals recorded against the finding rather than fixed here:
the power-action HTTP paths (/power-on/..., /stay/... GETs) are still
unauthenticated LAN endpoints, and PCON URLs still come from the wire
of (now authenticated) builders since sai-power has no conf-defined
PCON URL path to move them to.
diff --git a/etc-sai-EXAMPLE/builder/conf b/etc-sai-EXAMPLE/builder/conf index 4f9ee48..5a95e47 100644 --- a/etc-sai-EXAMPLE/builder/conf +++ b/etc-sai-EXAMPLE/builder/conf @@ -4,7 +4,8 @@ "host": "bionic-noi", # the fleet-wide secret sai-server's "link-key" pvo also has; - # the server refuses our connection without it + # the server refuses our connection without it, and it is + # presented to sai-power at registration # "link-key": "34c8e17b90d2a6f5c1e8430b76af2915e0d4cbf68a27e19dd5b0f3c6e48a71d2", diff --git a/etc-sai-EXAMPLE/power/conf b/etc-sai-EXAMPLE/power/conf index bc4947c..157508f 100644 --- a/etc-sai-EXAMPLE/power/conf +++ b/etc-sai-EXAMPLE/power/conf @@ -2,7 +2,8 @@ "perms": "sai:nobody", # the fleet-wide secret sai-server's "link-key" pvo also has; - # the server refuses our connection without it + # the server refuses our connection without it, and builders + # must present it to register with us # "link-key": "34c8e17b90d2a6f5c1e8430b76af2915e0d4cbf68a27e19dd5b0f3c6e48a71d2", diff --git a/src/builder/b-power.c b/src/builder/b-power.c index e31d774..16f8ea8 100644 --- a/src/builder/b-power.c +++ b/src/builder/b-power.c @@ -109,6 +109,17 @@ saib_power_client_state(void *userobj, void *sh, lws_ss_constate_t state, if (builder.power_monitor_url) lws_strncpy(r.power_monitor_url, builder.power_monitor_url, sizeof(r.power_monitor_url)); + /* + * sai-power refuses the registration without the fleet link + * secret + */ + if (!builder.link_key) + lwsl_err("%s: no link-key in conf, sai-power will " + "refuse our registration\n", __func__); + else + lws_strncpy(r.secret, builder.link_key, + sizeof(r.secret)); + /* Add platforms */ lws_start_foreach_dll(struct lws_dll2 *, d, builder.sai_plat_owner.head) { sai_plat_t *sp = lws_container_of(d, sai_plat_t, sai_plat_list); diff --git a/src/common/include/private.h b/src/common/include/private.h index 8dc7d8e..ff6ba46 100644 --- a/src/common/include/private.h +++ b/src/common/include/private.h @@ -313,6 +313,9 @@ struct sai_nspawn { lws_sorted_usec_list_t sul_mirror; lws_sorted_usec_list_t sul_task_cancel; + /* builder: sai_artifact_t of uploads still in flight for this ns */ + lws_dll2_owner_t artifact_owner; + sai_plat_t *sp; /* the sai_plat */ struct sai_plat_server *spm; /* the sai plat / server with the ss / wsi */ @@ -345,6 +348,7 @@ struct sai_nspawn { uint8_t user_cancel:1; uint8_t user_killed:1; uint8_t reap_cb_called:1; + uint8_t destroying:1; }; /* @@ -870,6 +874,7 @@ typedef struct sai_builder_registration { char power_off_type[16]; char power_off_url[128]; char power_monitor_url[128]; + char secret[129]; /* fleet link-key (wire only) */ } sai_builder_registration_t; typedef struct tasmota_data { @@ -971,7 +976,7 @@ extern const lws_struct_map_t lsm_build_metric[14], lsm_plat[14], /* +1 for pcon */ lsm_builder_platform[1], - lsm_builder_registration[9], + lsm_builder_registration[10], lsm_schema_sq3_map_power_controller[1], lsm_schema_sq3_map_controlled_builder[1], lsm_schema_builder_registration[1], @@ -1038,7 +1043,7 @@ sai_is_safe_ref(const char *s); /* * The .sai.json "artifacts" field is repo-controlled and reaches the * builder as a comma-separated list of globs, possibly with a path part - * before the first '*', eg "build/*.rpm,*.tar.gz". The builder scans + * before the first '*', eg "build/ *.rpm,*.tar.gz". The builder scans * them under the per-instance build dir and renames what it matches into * its uploads dir, so a pattern whose path part climbs out of the * instance dir (a ".." component, an absolute pattern, or a windows diff --git a/src/common/struct-metadata.c b/src/common/struct-metadata.c index 92072d3..3d6d73d 100644 --- a/src/common/struct-metadata.c +++ b/src/common/struct-metadata.c @@ -478,6 +478,8 @@ const lws_struct_map_t lsm_builder_registration[] = { LSM_CARRAY(sai_builder_registration_t, power_off_type, "power_off_type"), LSM_CARRAY(sai_builder_registration_t, power_off_url, "power_off_url"), LSM_CARRAY(sai_builder_registration_t, power_monitor_url, "power_monitor_url"), + /* deliberately not persisted in the sq3 map */ + LSM_CARRAY(sai_builder_registration_t, secret, "secret"), }; const lws_struct_map_t lsm_schema_builder_registration[] = { diff --git a/src/power/p-http-api.c b/src/power/p-http-api.c index 64ca326..72b9cee 100644 --- a/src/power/p-http-api.c +++ b/src/power/p-http-api.c @@ -289,6 +289,25 @@ local_srv_rx(void *userobj, const uint8_t *buf, size_t len, int flags) if (g->a.top_schema_index == 0) { sai_builder_registration_t *r = (sai_builder_registration_t *)g->a.dest; + /* + * Registration creates and retargets PCONs, rebinds + * builder->PCON mappings and sets the URLs sai-power will + * then fetch from its own network position, so it has to + * prove the fleet link secret (the same "link-key" conf + * sai-server authenticates builders with) before anything + * from it is applied. Fail closed when we have no key + * configured, and compare in constant time. + */ + if (!power.link_key || + strlen(r->secret) != strlen(power.link_key) || + lws_timingsafe_bcmp(r->secret, power.link_key, + (uint32_t)strlen(power.link_key))) { + lwsl_ss_warn(h, "registration without a valid link " + "secret, dropping"); + lwsac_free(&g->a.ac); + return LWSSSSRET_DISCONNECT_ME; + } + lwsl_ss_notice(h, "Registered builder '%s' on pcon '%s'", r->builder_name, r->power_controller_name);
Page fetched 0s ago, creation time: 4ms (vhost etag hits: 0%, cache hits: 0%)