Manual

Turn on OpenBeken web auth and manage scripts over HTTP

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.

Tested 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:
    <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.

    #!/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.

    $ ./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.

    #!/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.

    $ ./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):

$ 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:

$ 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:

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)CauseFix
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 adminGive 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 answeringNone 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.

Filed under: openbeken, tuya, smart-home, security, curl

Sources#

  • OpenBK7231T_App at tag 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).
  • 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).