# Turn on OpenBeken web auth and manage scripts over HTTP

> Enable HTTP Basic auth on an OpenBeken plug from a script without touching Wi-Fi, check 401/200, then upload .bat scripts and restart with the password.

Published 2026-10-17. HTML version: https://guides.tsvinc.dev/openbeken-web-auth-http-api/


Two shell scripts switch on an OpenBeken plug's web password over HTTP without touching its Wi-Fi settings, prove that requests without the password get 401, then upload `.bat` scripts to the plug's filesystem and restart it, with the password kept out of every command line.

> **Scope** — Verified on: RZTK C3680W plug (OEM FrankEver FK-PW802EC-F, Tuya WB2S module, BK7231T) running OpenBeken 1.18.318 (OpenBK7231T build); client Fedora 44, curl 8.18.0, bash 5.3.9 (2026-10-16).
> **Needs:** an OpenBeken plug on your LAN with web auth still off; `bash` and `curl` on the client; the list of everything that polls the plug (Home Assistant, a monitor, a script).
> **Time:** 15 min. **Risk:** pollers without the password start getting 401 the moment auth goes on. The restart in the upload step drops the relay for a few seconds, so whatever is plugged into it loses power briefly. A lost password locks you out of the web UI and API. Undo is one request, but it was not exercised (see Undo).

What this protects and what it does not: OpenBeken's auth is HTTP Basic over plain HTTP, and the enable request carries the new password in a GET query string. It keeps a curious device on the LAN away from the relay, the firmware update endpoint and the flash reader. It does not stop anyone who can capture the plug's traffic.

## Before you start

- **Placeholders**, used everywhere below:
  - `<plug>`: the plug's IP address or DNS name.
  - `<web-password>`: the new web password. Letters and digits only, 5 to 32 characters (see Notes for why).
- **The firmware has web auth compiled in.** The Wi-Fi configuration page (Config → Configure WiFi) shows an "Enable web authentication" checkbox and an "Admin Password" field. In 1.18.318 the feature is on at build time (`#define ALLOW_WEB_PASSWORD 1`, `src/new_pins.h:1732`) and off at runtime until a password is set: an empty password skips the check (`src/httpserver/http_basic_auth.c:12-14`).
- **Relay start state.** The upload step restarts the plug. If you need the relay back on after a restart, set it first with `/cm?cmnd=SetStartValue%201%201`; my plug has that setting, which is why the relay channel reads 1 in Verify.
- **File layout.** The scripts read the password from `../router/secrets.env`, relative to their own directory. Keep this shape or edit the `.` line in both scripts:
  ```text
  <dir>/plug/enable-auth.sh
  <dir>/plug/upload-scripts.sh
  <dir>/plug/led.bat, prot.bat, autoexec.bat
  <dir>/router/secrets.env
  ```
  `router/secrets.env` holds one line, `OBSERVER_PLUG_PASSWORD=<web-password>`. Make it mode 600 and keep it out of git. The `.bat` files are the ones you want on the plug; mine are in the public repo listed under Sources.
- **Backup.** Save a copy of every script already on the plug before you overwrite it: `GET http://<plug>/api/lfs/<name>` returns the file; while auth is off it needs no password.

## Steps

1. **Give every client that polls the plug the password, before anything else.**
   The password is set in memory at once and saved to flash shortly after, so auth applies from the next request. Any poller still sending anonymous requests gets 401 from then on. The username is fixed: `admin` (`http_basic_auth.c:29`). In my setup the poller is nut-observer, which reads `OBSERVER_PLUG_PASSWORD` from its environment and sends it with its voltage polls and LED commands. I updated it first. While auth is still off, a request that carries a password is accepted like any other, so clients can be switched over early.

2. **Save `plug/enable-auth.sh`.**
   ```bash
   #!/bin/bash
   # Turn on OpenBeken web authentication (HTTP Basic, user admin) with OBSERVER_PLUG_PASSWORD from
   # router/secrets.env. Run AFTER router/update-observer.sh, so the observer already sends the password.
   # Uses /cfg_wifi_set with only the password fields: Wi-Fi settings untouched, no module reset.
   # Undo: same form with web_admin_password_enabled absent (clears the password), sent with the password.
   set -euo pipefail
   cd "$(dirname "$0")"
   IP=${IP:-<plug>}
   set -a; . ../router/secrets.env; set +a
   [ -n "${OBSERVER_PLUG_PASSWORD:-}" ] || { echo "ABORT: OBSERVER_PLUG_PASSWORD missing (run router/update-observer.sh first)"; exit 1; }
   # URL with the password goes through curl's stdin config, not argv
   printf 'url = "http://%s/cfg_wifi_set?web_admin_password_enabled=1&web_admin_password=%s"\n' "$IP" "$OBSERVER_PLUG_PASSWORD" \
     | curl -fsS --max-time 10 -K - | /usr/bin/grep -o 'Web password has been changed\|No changes detected' || true
   sleep 2
   code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "http://$IP/cm?cmnd=STATUS%208")
   echo "without password: HTTP $code (want 401)"
   code=$(printf 'user = "admin:%s"\n' "$OBSERVER_PLUG_PASSWORD" | curl -s -o /dev/null -w '%{http_code}' --max-time 10 -K - "http://$IP/cm?cmnd=STATUS%208")
   echo "with password:    HTTP $code (want 200)"
   ```
   Replace `<plug>` on the `IP=` line, or leave it and run with `IP=<address>` in front. `router/update-observer.sh` in the comments is my step 1; yours is whatever you did there.

   Why it is safe for the Wi-Fi settings: the request sends only `web_admin_password_enabled` and `web_admin_password`. `http_fn_cfg_wifi_set` changes the SSID and Wi-Fi password only when the `ssid`, `pass`, `ssid2` or `pass2` arguments are present (`src/httpserver/http_fns.c:1889-1903`). It schedules a module reset only when one of those changed (`http_fns.c:1920-1926`). The web password itself is set without a reset (`http_fns.c:1905-1913`).

   Why the password stays out of argv: `printf` is a bash builtin, and curl reads the URL and the `user =` line as a config file from stdin (`-K -`). Neither the password nor the URL that contains it appears in any process's command line.

3. **Run it.**
   ```console
   $ ./plug/enable-auth.sh
   Web password has been changed
   No changes detected
   without password: HTTP 401 (want 401)
   with password:    HTTP 200 (want 200)
   ```
   Expected: all four lines as above. `No changes detected` is the Wi-Fi half of the reply page (`WiFi: No changes detected.`, `http_fns.c:1921`): Wi-Fi settings unchanged, no reset. If the first line is missing, read curl's error above the output; a password under 5 characters is refused and prints only `No changes detected` (Notes). If `without password` says 200, auth is not on.

4. **Save `plug/upload-scripts.sh`.**
   ```bash
   #!/bin/bash
   # Upload led.bat, prot.bat and autoexec.bat to the plug's filesystem, then restart it so they run.
   # The AC180's wall input runs through this plug: the restart drops the relay for a few seconds.
   # Sends the web password from router/secrets.env (OBSERVER_PLUG_PASSWORD) if set; harmless while auth is off.
   set -euo pipefail
   cd "$(dirname "$0")"
   IP=${IP:-<plug>}
   set -a; . ../router/secrets.env; set +a
   AUTH=()
   [ -n "${OBSERVER_PLUG_PASSWORD:-}" ] && AUTH=(-K -)   # credentials via stdin, not argv
   cr(){ if [ ${#AUTH[@]} -gt 0 ]; then printf 'user = "admin:%s"\n' "$OBSERVER_PLUG_PASSWORD" | curl -fsS --max-time 10 "${AUTH[@]}" "$@"; else curl -fsS --max-time 10 "$@"; fi; }
   for f in led.bat prot.bat autoexec.bat; do
     cr -X POST --data-binary @"$f" "http://$IP/api/lfs/$f" -o /dev/null && echo "uploaded $f"
   done
   cr "http://$IP/cm?cmnd=restart" -o /dev/null || true
   echo "restarting; waiting for the plug"
   for _ in $(seq 1 30); do
     sleep 3
     if out=$(cr "http://$IP/cm?cmnd=STATUS%208" 2>/dev/null); then echo "$out"; exit 0; fi
   done
   echo "plug not back after 90 s"; exit 1
   ```
   Edit the file list in the `for` line to match your scripts. `POST /api/lfs/<name>` writes the file (`src/httpserver/rest_interface.c:283`). The whole `/api/` tree is registered with `auth_required = 1` (`rest_interface.c:79-80`), so uploads need the password.

5. **Warn whatever hangs off the relay, then run it.** The restart drops the relay for a few seconds. On my plug that cuts the power station's wall input, so the station briefly runs on battery.
   ```console
   $ ./plug/upload-scripts.sh
   uploaded led.bat
   uploaded prot.bat
   uploaded autoexec.bat
   restarting; waiting for the plug
   {"StatusSNS":{"Time":"2026-10-16T20:39:44","ENERGY":{"Power": 381.000000,"ApparentPower": 454.151275,"ReactivePower": 247.168716,"Factor":0.838927,"Voltage":232.121780,"Current":1.956521,"Frequency":0.000000,"ConsumptionTotal":5332.825683,"Yesterday": 0.047083,"ConsumptionLastHour":0.000000},"ESP32":{"Temperature": 49.9},"TempUnit":"C"}}
   ```
   Expected: one `uploaded` line per file, then one `STATUS 8` JSON line once the plug answers again with the password. `plug not back after 90 s` means it did not answer with the password within 90 s.

## Verify

Run from `<dir>` once the restart has finished. The scripts on the plug match the local copies, the plug has just rebooted, the relay is on (channel 1), the LED mode is 1 (channel 6, my `led.bat`), and the protection script has its 170 V under-voltage threshold and no trip (channels 11 and 12, my `prot.bat`):

```console
$ set -a; . router/secrets.env; set +a; A(){ printf 'user = "admin:%s"\n' "$OBSERVER_PLUG_PASSWORD" | curl -fsS --max-time 10 -K - "$@"; }; for f in led.bat prot.bat; do A http://<plug>/api/lfs/$f | cmp -s - plug/$f && echo "$f same" || echo "$f DIFF"; done; A 'http://<plug>/cm?cmnd=STATUS%2011' | python3 -c 'import json,sys;print("uptime",json.load(sys.stdin).get("StatusSTS",{}).get("UptimeSec"))'; for c in 1 6 11 12; do printf "ch$c="; A "http://<plug>/cm?cmnd=GetChannel%20$c"; echo; done
led.bat same
prot.bat same
uptime 19
ch1=1
ch6=1
ch11=170
ch12=0
```

And without the password, both the command API and the filesystem API refuse, with the realm from `http_basic_auth.c:50`:

```console
$ curl -s -D - -o /dev/null --max-time 10 "http://<plug>/cm?cmnd=STATUS%208"
HTTP/1.1 401 Unauthorized
Connection: close
WWW-Authenticate: Basic realm="OpenBeken HTTP Server"

$ curl -s -o /dev/null -w '%{http_code}\n' --max-time 10 "http://<plug>/api/lfs/led.bat"
401
```

## Undo / rollback

`[UNVERIFIED: rollback not exercised]` From the source: a `/cfg_wifi_set` request that has no `web_admin_password_enabled` argument clears the web password (`http_fns.c:1915-1917`). The request itself must carry the password, because with auth on `/cfg_wifi_set` is served only after the catch-all check (`src/httpserver/new_http.c:938-942`, handler at `new_http.c:998`). With no Wi-Fi arguments it does not reset the module, as in step 2. From `<dir>/plug`:

```bash
set -a; . ../router/secrets.env; set +a
printf 'url = "http://%s/cfg_wifi_set"\nuser = "admin:%s"\n' "<plug>" "$OBSERVER_PLUG_PASSWORD" | curl -fsS --max-time 10 -K -
```

Sending `web_admin_password_enabled=0` does not clear it; only leaving the argument out does (`http_fns.c:1905-1917`). Then check `curl -s -o /dev/null -w '%{http_code}\n' "http://<plug>/cm?cmnd=STATUS%208"` prints 200. Uploaded scripts are undone by uploading the backup copies from Before you start with `upload-scripts.sh`.

## Failure modes

| Symptom (exact message) | Cause | Fix |
|---|---|---|
| A poller's requests fail with `401` after step 3 (`without password: HTTP 401` is the same response) | The client still sends no credentials, or not as user `admin` | Give it the password with HTTP Basic user `admin` (step 1). Nothing to change on the plug. |
| A poller's request fails during step 5; my monitor logged `set mode 1 failed: Get "http://<plug>:80/cm?cmnd=SetChannel%20…` (the error tail wasn't captured) | The plug is rebooting and not answering | None needed. My monitor's next LED command, about 8 s later, went through. |

## Field notes

- **2026-10-16 21:05 UTC, same plug, OpenBeken 1.18.318, Fedora 44 client, curl 8.18.0:** re-checked from the client with no password. `STATUS 8` and `GET /api/lfs/led.bat` both returned 401 with the OpenBeken realm (pasted in Verify). Auth had stayed on through the 20:39 restart.
- **2026-10-16 20:39 UTC, same plug and client:** ran `upload-scripts.sh` with auth on, to install new LED patterns. The three uploads went through with the password, the plug answered again, and the check right after showed uptime 19 s, `led.bat` and `prot.bat` byte-identical to my copies, relay on, no trip. `autoexec.bat` was unchanged and I did not compare it.
- **2026-10-16 10:56 UTC, same plug and client:** ran `enable-auth.sh`. Output as in step 3. I read the monitor's log twice over the next 1 min 40 s: no failed voltage polls and no failed LED sends, so step 1 had worked.
- **2026-10-16 10:38 UTC, same plug and client, auth still off:** first run of `upload-scripts.sh`, after giving the monitor the password (about 10:23 UTC). The script sent the password anyway; with no password set, the plug accepted the requests like anonymous ones. The plug was back with uptime 42 s at the check; `led.bat` and `prot.bat` matched. During the restart one LED command from the monitor failed (logged as `set mode 1 failed: Get …`, error tail not captured), and the retry at 10:38:00 got through. The monitor never reported the power station on battery: it needs two on-battery readings 20 s apart, and the relay was open for less than that.
- **2026-10-16, morning, before any of this:** my notes and a draft of the plug post said OpenBeken had no web login. The 1.18.318 source (`http_basic_auth.c`, `http_fns.c`) showed one: off by default, with its checkbox on the Wi-Fi configuration page.

## Notes

- **Password characters.** `enable-auth.sh` puts the password unencoded into a URL query string, and both scripts put it inside a double-quoted curl config value. `&`, `#`, `%`, space, `"` or `\` break one or the other. Use letters and digits. OpenBeken rejects passwords under 5 characters with `Web password needs to be at least 5 characters long!` (`http_fns.c:1908-1909`, from source, not hit). It stores at most 32 (`char webPassword[33]`, `new_pins.h:1745`) and compares 32 (`http_basic_auth.c:31`).
- **The web UI path also works,** but it is the same Wi-Fi form, and it submits SSID and Wi-Fi password along with the auth fields (`http_fns.c:1805-1829`). The script exists to send only the two auth fields.
- **Any `/cfg_wifi_set` without `web_admin_password_enabled` clears the password,** including a script or bookmark that sets only SSID and Wi-Fi password (`http_fns.c:1915-1917`).
- **Logged-in users can read the password:** the Wi-Fi page pre-fills the Admin Password field with the stored value (`http_fns.c:1825`).
- **Not covered by auth:** commands received over MQTT, if MQTT is configured (`new_mqtt.c:826,844,854`); they never pass through HTTP auth. Also endpoints registered by optional drivers with `auth_required = 0`, for example SSDP's `/ssdp.xml` and `/obkdevicelist`, and the WeMo and Hue emulation pages (`src/driver/drv_ssdp.c:438,445`, `drv_wemo.c:316-319`, `drv_hue.c:253`). They answer only when that driver is running. I did not check which drivers run on mine.
- **Safe mode** skips auth only if flag 44 (`OBK_FLAG_HTTP_DISABLE_AUTH_IN_SAFE_MODE`, `new_pins.h:1419`) was set beforehand (`http_basic_auth.c:12`). Without it, the recovery paths the source shows are: the `clearConfig` / `clearAll` commands (`cmd_main.c:1116,1126`), which reset the whole config including the password and the Wi-Fi settings (`new_cfg.c:110`), sent over MQTT if MQTT is configured, or a serial reflash. None of them tested.
- **Next layer,** not done here: a separate IoT network that only the router and the monitor can reach.
- The story of this plug (reflash, pin map, LED and protection scripts): [The pin template was for a different plug](https://tsvinc.dev/posts/tuya-plug-openbeken-grid-canary/).

## Sources

- [OpenBK7231T_App at tag 1.18.318](https://github.com/openshwprojects/OpenBK7231T_App/tree/1.18.318) — OpenBeken source; every `file:line` above is from this tag (the `httpserver/` and core files cited here were fetched from GitHub and compared byte for byte with my local checkout; the `driver/` lines were read in the checkout only).
  - [`src/httpserver/http_basic_auth.c`](https://github.com/openshwprojects/OpenBK7231T_App/blob/1.18.318/src/httpserver/http_basic_auth.c) — the check: empty password = open, user `admin`, 32-character compare, 401 with the realm.
  - [`src/httpserver/new_http.c`](https://github.com/openshwprojects/OpenBK7231T_App/blob/1.18.318/src/httpserver/new_http.c) — per-callback `auth_required` check (l. 929) and the catch-all check (l. 938-942) in front of `/cfg_wifi_set` and `/cm`.
  - [`src/httpserver/rest_interface.c`](https://github.com/openshwprojects/OpenBK7231T_App/blob/1.18.318/src/httpserver/rest_interface.c) — `/api/` registered with auth (l. 79-80); LFS upload (l. 283).
  - [`src/httpserver/http_fns.c`](https://github.com/openshwprojects/OpenBK7231T_App/blob/1.18.318/src/httpserver/http_fns.c) — `http_fn_cfg_wifi_set` (l. 1874-1932) and the Wi-Fi form (l. 1805-1829).
  - [`src/new_pins.h`](https://github.com/openshwprojects/OpenBK7231T_App/blob/1.18.318/src/new_pins.h) — `ALLOW_WEB_PASSWORD`, `webPassword[33]`, flag 44.
- [openbeken-grid-canary-plug](https://gitlab.com/random-home-utils/openbeken-grid-canary-plug) — my plug's `led.bat`, `prot.bat`, `autoexec.bat`, pin map and template (the two shell scripts above are not in it).

