<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>CHG-0013: Tenant Wi-Fi PPSK Keys on Deevnet Infrastructure Platform</title><link>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/</link><description>Recent content in CHG-0013: Tenant Wi-Fi PPSK Keys on Deevnet Infrastructure Platform</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/index.xml" rel="self" type="application/rss+xml"/><item><title>1. Recon</title><link>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/01-recon/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/01-recon/</guid><description>&lt;h1 id="phase-1-recon">
 Phase 1: Recon
 &lt;a class="anchor" href="#phase-1-recon">#&lt;/a>
&lt;/h1>
&lt;p>&lt;strong>Read-only, unauthenticated, no writes.&lt;/strong> Done 2026-09-18, before any code was written, because
three of its answers changed what the automation had to do.&lt;/p>
&lt;h2 id="run">
 Run
 &lt;a class="anchor" href="#run">#&lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>curl -sk &lt;span style="color:#e6db74">&amp;#34;https://10.20.99.40:8043/v3/api-docs/00%20All&amp;#34;&lt;/span> -o omada-spec.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>1,856 paths, 3,360 schemas, controller 6.3.0.45. No credentials are needed: the spec endpoint is
unauthenticated, the same one &lt;code>omada-wireless.yml&lt;/code>&amp;rsquo;s preflight already reads.&lt;/p>
&lt;h2 id="verify">
 Verify
 &lt;a class="anchor" href="#verify">#&lt;/a>
&lt;/h2>
&lt;p>The five questions below are answered, and each is written down here rather than inferred later.&lt;/p></description></item><item><title>2. Substrate</title><link>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/02-substrate/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/02-substrate/</guid><description>&lt;h1 id="phase-2-create-the-profile-and-the-ssid">
 Phase 2: Create the profile and the SSID
 &lt;a class="anchor" href="#phase-2-create-the-profile-and-the-ssid">#&lt;/a>
&lt;/h1>
&lt;p>&lt;strong>Needs a maintenance window.&lt;/strong> This is the only phase that touches a live radio.&lt;/p>
&lt;p>Merged: &lt;code>ansible-collection-deevnet.net&lt;/code> #26, &lt;code>ansible-inventory-deevnet&lt;/code> #41.&lt;/p>
&lt;h2 id="what-changed-in-inventory">
 What changed in inventory
 &lt;a class="anchor" href="#what-changed-in-inventory">#&lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">iot&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># ...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">wifi_ssid&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;DVNTM-IOT&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">wifi_security&lt;/span>: &lt;span style="color:#ae81ff">ppsk&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">wifi_ppsk_profile&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;DVNTM-IOT&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>and &lt;code>deevnet_wifi_psk.iot&lt;/code> was &lt;strong>deleted&lt;/strong>. A segment that says nothing about &lt;code>wifi_security&lt;/code> keeps
a shared key, so the other three wireless segments needed no edit at all — which is what keeps this
change away from &lt;code>DVNTM&lt;/code>.&lt;/p></description></item><item><title>3. API</title><link>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/03-api/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/03-api/</guid><description>&lt;h1 id="phase-3-the-api-issues-keys">
 Phase 3: The API issues keys
 &lt;a class="anchor" href="#phase-3-the-api-issues-keys">#&lt;/a>
&lt;/h1>
&lt;p>Merged: &lt;code>deevnet-provisioning-api&lt;/code> #9 (v0.3.0), &lt;code>ansible-collection-deevnet.mgmt&lt;/code> #29.&lt;/p>
&lt;p>&lt;strong>Blocked on the Owner&lt;/strong> creating the API&amp;rsquo;s own Open API client. Until then v0.3.0 deploys happily
with Wi-Fi keys off — that is deliberate, and tested.&lt;/p>
&lt;h2 id="run">
 Run
 &lt;a class="anchor" href="#run">#&lt;/a>
&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Owner, in the controller UI:&lt;/strong> Global View → Settings → Platform Integration → Open API. Create
a second client, separate from Ansible&amp;rsquo;s.&lt;/li>
&lt;li>Vault it as &lt;code>vault_omada_api_client_id&lt;/code> / &lt;code>vault_omada_api_client_secret&lt;/code> in
&lt;code>group_vars/network_controllers&lt;/code>.&lt;/li>
&lt;li>In &lt;code>mobile/group_vars/deevnet_api/vars.yml&lt;/code>, set &lt;code>deevnet_api_omada_url&lt;/code> and add
&lt;code>omada_client_id&lt;/code> / &lt;code>omada_client_secret&lt;/code> to &lt;code>deevnet_api_backends&lt;/code>, read cross-group the way the
router key already is.&lt;/li>
&lt;li>Stage and deploy:&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>cd deevnet-provisioning-api &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> git tag v0.3.0 &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> make stage
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd ../ansible-collection-deevnet.mgmt &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> ansible-playbook playbooks/deevnet-api.yml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="verify">
 Verify
 &lt;a class="anchor" href="#verify">#&lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>curl -s https://api.mobile.deevnet.net/version &lt;span style="color:#75715e"># v0.3.0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># operator token, against a scratch tenant:&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl -sk -H &lt;span style="color:#e6db74">&amp;#34;Authorization: Bearer &lt;/span>$OP&lt;span style="color:#e6db74">&amp;#34;&lt;/span> -X POST &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> https://api.mobile.deevnet.net/v1/tenants/tdemo/wifi-keys &lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span> -d &lt;span style="color:#e6db74">&amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;scratch&amp;#34;,&amp;#34;trust_class&amp;#34;:&amp;#34;iot&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>The response carries &lt;code>ssid: DVNTM-IOT&lt;/code>, &lt;code>vlan: 30&lt;/code> and a &lt;code>psk&lt;/code>.&lt;/li>
&lt;li>The controller UI shows &lt;strong>one key&lt;/strong> in the &lt;code>DVNTM-IOT&lt;/code> profile, named &lt;code>tdemo-scratch&lt;/code>, bound to
VLAN 30.&lt;/li>
&lt;li>&lt;code>DELETE&lt;/code> removes it, and the profile is empty again.&lt;/li>
&lt;li>&lt;strong>Grep the API logs for that &lt;code>psk&lt;/code> and find nothing.&lt;/strong>&lt;/li>
&lt;li>Restart the API and issue another key, to exercise a fresh token.&lt;/li>
&lt;/ul>
&lt;h2 id="outcome--ran-2026-09-18">
 Outcome — ran 2026-09-18
 &lt;a class="anchor" href="#outcome--ran-2026-09-18">#&lt;/a>
&lt;/h2>
&lt;p>&lt;strong>Deployed and issuing keys.&lt;/strong> &lt;code>GET /version&lt;/code> returns &lt;code>v0.3.0&lt;/code> (commit &lt;code>c7afbe9&lt;/code>), &lt;code>readyz&lt;/code> reports
the database ok, and the deploy was &lt;code>failed=0&lt;/code> with 14 changed tasks — including the KV secret
rewrite that gave the API its Omada credential.&lt;/p></description></item><item><title>4. Tenant</title><link>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/04-tenant/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/04-tenant/</guid><description>&lt;h1 id="phase-4-a-tenant-issues-its-own-key">
 Phase 4: A tenant issues its own key
 &lt;a class="anchor" href="#phase-4-a-tenant-issues-its-own-key">#&lt;/a>
&lt;/h1>
&lt;p>Merged: &lt;code>terraform-provider-deevnet&lt;/code> #3.&lt;/p>
&lt;p>&lt;strong>No hardware needed.&lt;/strong> This phase is where the restore guarantee is proven, and it is proven by
breaking it on purpose.&lt;/p>
&lt;h2 id="run">
 Run
 &lt;a class="anchor" href="#run">#&lt;/a>
&lt;/h2>
&lt;p>In the tenant&amp;rsquo;s own Terraform, for example &lt;code>/srv/eds/infra/deevnet-tenant-eds&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-hcl" data-lang="hcl">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">resource&lt;/span> &lt;span style="color:#e6db74">&amp;#34;deevnet_iot_wifi_key&amp;#34; &amp;#34;devices&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tenant &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">deevnet_tenant&lt;/span>.&lt;span style="color:#66d9ef">this&lt;/span>.&lt;span style="color:#66d9ef">name&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;devices&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> trust_class &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;iot&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">output&lt;/span> &lt;span style="color:#e6db74">&amp;#34;wifi&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> value &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ssid &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">deevnet_iot_wifi_key&lt;/span>.&lt;span style="color:#66d9ef">devices&lt;/span>.&lt;span style="color:#66d9ef">ssid&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> psk &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">deevnet_iot_wifi_key&lt;/span>.&lt;span style="color:#66d9ef">devices&lt;/span>.&lt;span style="color:#66d9ef">psk&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sensitive &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Build and install the provider, delete the stale lock, then apply:&lt;/p></description></item><item><title>5. A client joins</title><link>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/05-device/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/05-device/</guid><description>&lt;h1 id="phase-5-a-client-joins-with-a-tenants-key">
 Phase 5: A client joins with a tenant&amp;rsquo;s key
 &lt;a class="anchor" href="#phase-5-a-client-joins-with-a-tenants-key">#&lt;/a>
&lt;/h1>
&lt;p>&lt;strong>Needs someone at the site with a wireless client.&lt;/strong> Everything before this is remote.&lt;/p>
&lt;p>What this phase proves is narrow and worth stating: &lt;strong>that a tenant&amp;rsquo;s key authenticates, and that
the key decides the VLAN.&lt;/strong> Any wireless client does that. It does not have to be the device the
tenant will eventually run.&lt;/p>
&lt;h2 id="run">
 Run
 &lt;a class="anchor" href="#run">#&lt;/a>
&lt;/h2>
&lt;p>Get the credential from the tenant, not from the controller:&lt;/p></description></item><item><title>6. Close-out</title><link>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/06-close-out/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/06-close-out/</guid><description>&lt;h1 id="phase-6-close-out">
 Phase 6: Close-out
 &lt;a class="anchor" href="#phase-6-close-out">#&lt;/a>
&lt;/h1>
&lt;h2 id="run">
 Run
 &lt;a class="anchor" href="#run">#&lt;/a>
&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Correct the pages this change makes wrong or newly right:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>architecture/substrate/networking.md&lt;/code> — the Wireless section names no SSIDs and no security
model. It should carry the four, and say that on &lt;code>DVNTM-IOT&lt;/code> the key decides the VLAN.&lt;/li>
&lt;li>&lt;code>platforms/network/access-point/_index.md&lt;/code> — &amp;ldquo;SSID Design&amp;rdquo; lists &lt;code>Management / IoT / Guest&lt;/code>,
which is not this site, and points at a standard that documents neither. Replace with the four
real SSIDs and the ownership line: inventory owns the SSID and the profile, the API owns the
keys inside it.&lt;/li>
&lt;li>&lt;code>platforms/network/network-controllers/_index.md&lt;/code> — record the &lt;strong>second&lt;/strong> Open API client and
what it is for.&lt;/li>
&lt;li>&lt;code>runbook/recovery/console-recovery/wireless-ap.md&lt;/code> §6–7 — drop the standalone-UI and
&lt;code>make migration-omada-ssids&lt;/code> assumptions; recovery is &lt;code>make wireless APPLY=1 ADOPT=1&lt;/code> then
&lt;code>make wireless APPLY=1&lt;/code>. The §7 check of &lt;code>DVNTM-IOT&lt;/code> now actually works.&lt;/li>
&lt;/ul>
&lt;p>That recovery page also gets the sentence that matters most operationally:
&lt;strong>recovery does not re-issue tenant keys.&lt;/strong> They survive an AP rebuild in the profile, and if the
profile itself is lost each tenant runs &lt;code>terraform apply&lt;/code> and gets the same key back — so no
device visit. ADR-0012 §5, made concrete.&lt;/p></description></item><item><title>Undo</title><link>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/undo/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://deevnet.github.io/deevnet-docs/docs/changes/2026/0013-tenant-wifi-ppsk-keys/undo/</guid><description>&lt;h1 id="chg-0013-undo">
 CHG-0013: Undo
 &lt;a class="anchor" href="#chg-0013-undo">#&lt;/a>
&lt;/h1>
&lt;p>Written before the change ran. Each phase is reversible on its own, and the order here is the
reverse of the order they run in.&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Phase&lt;/th>
 &lt;th>Reversal&lt;/th>
 &lt;th>Leaves behind&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>5. Device&lt;/td>
 &lt;td>Reflash the stand&amp;rsquo;s previous configuration&lt;/td>
 &lt;td>Nothing depends on the stand&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4. Tenant&lt;/td>
 &lt;td>&lt;code>terraform destroy -target=deevnet_iot_wifi_key.devices&lt;/code>, or remove the block and apply&lt;/td>
 &lt;td>The key is revoked at the controller&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3. API&lt;/td>
 &lt;td>Redeploy &lt;code>deevnet_api_version: v0.2.6&lt;/code>; delete any test key from the profile&lt;/td>
 &lt;td>Unsetting &lt;code>deevnet_api_omada_url&lt;/code> alone stops key issuance without a rollback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2. Substrate&lt;/td>
 &lt;td>Controller UI: delete the SSID &lt;code>DVNTM-IOT&lt;/code>, then the PPSK profile &lt;code>DVNTM-IOT&lt;/code>. &lt;code>git revert&lt;/code> the inventory commit, restoring &lt;code>deevnet_wifi_psk.iot&lt;/code>, then &lt;code>make vault&lt;/code>&lt;/td>
 &lt;td>The VLAN 30 network object may stay; it is harmless and every other segment has one&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1. Recon&lt;/td>
 &lt;td>None — nothing was written&lt;/td>
 &lt;td>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="what-cannot-be-undone">
 What cannot be undone
 &lt;a class="anchor" href="#what-cannot-be-undone">#&lt;/a>
&lt;/h2>
&lt;p>&lt;strong>A revoked key.&lt;/strong> Once a tenant&amp;rsquo;s key is deleted and a new one issued, every device flashed with
the old one needs a physical visit. Reversing phases 3 and 4 in the order above avoids this: destroy
the Terraform resource, which revokes one key deliberately, rather than rolling the API back and
leaving keys stranded in a profile nothing manages.&lt;/p></description></item></channel></rss>