About
KWallet Secrets
NetworkManager keeps no copy of an agent-owned Wi-Fi password, and no copy of a VPN password at all: it asks a registered secret agent for them every time you connect. On a Plasma desktop that agent is plasma-nm, which reads them out of KWallet. On a Noctalia session there is no such agent, so every one of those networks and every VPN asks you to type a password you already saved. This plugin fills that gap: it runs a secret agent that answers NetworkManager out of KWallet, so they just connect.
Plugin
| Field | Value |
|---|---|
| ID | grassyloki/kwallet-secrets |
| Entries | service: service |
Requirements
python3, with thepython-dbusandpython-gobjectbindings. Both ship as distro packages (python-dbus/python3-dbus,python-gobject/python3-gi); nopipinstall and no virtualenv is involved.kwalletd6, the classic KWallet daemon. It provides theorg.kde.KWalletD-Bus interface this plugin reads. The newerksecretdserves the same wallet file over the Secret Service API but does not implement that interface, so it is not a substitute.pkill(from procps-ng) andsystemd-cat(from systemd), used to supervise the helper process and to route its output to the journal.- NetworkManager as the network stack, and a KWallet wallet that is unlocked.
kwallet-pamunlocks it at login; without it, the first lookup of the session raises KWallet's unlock dialog.
The plugin is compositor-agnostic: nothing in it depends on Hyprland, niri, or any particular Wayland session.
Usage
There is nothing to add to your bar and no panel to open. Enable the plugin and the service starts a background secret agent; from then on, saved Wi-Fi networks and VPNs whose passwords live in KWallet connect without prompting.
To check that it is working:
journalctl -b -t NetworkManager | grep 'agent registered'
journalctl --user -t noctalia-kwallet-secrets -f
The first command should list an agent named io.github.grassyloki.kwalletSecrets.
The second follows the plugin's own log, which prints one line per request: a
hit when the password came from the wallet, a miss when the wallet had no
entry for that network.
To see what the wallet actually holds, run the helper directly. It prints entry and key names only, never a password:
~/.local/state/noctalia/plugins/materialized/community/kwallet-secrets/scripts/kwallet-nm-agent.py \
--check --with-vpn --with-8021x
If a network or VPN still prompts, the usual causes are a locked wallet, a profile whose password was never saved to KWallet in the first place, or a stored password that is simply wrong ā see Notes.
Settings
Configure these under Settings, Plugins, KWallet Secrets. Changing any of them restarts the helper.
| Setting | Type | Default | Description |
|---|---|---|---|
wallet_name |
string |
(empty) | Wallet to read. Empty means whichever wallet KWallet has configured for network data, normally kdewallet. |
folder_name |
string |
Network Management |
Folder inside the wallet holding the entries. This is where plasma-nm puts them; change it only if you keep them somewhere else. |
app_id |
string |
Noctalia KWallet Secrets |
The name KWallet shows when it asks whether to grant access to the wallet. |
handle_8021x |
bool |
false |
Also answer 802-1x requests, for WPA-Enterprise networks such as eduroam. Off by default because those profiles often carry certificates that no wallet entry covers. |
handle_vpn |
bool |
true |
Also answer vpn requests ā OpenVPN, vpnc, openconnect, L2TP and the rest ā and NetworkManager's own wireguard setting, including per-peer preshared keys. On by default, because a VPN secret is agent-owned in every profile plasma-nm imports and so prompts on every single connect. |
unlock_prompt |
bool |
true |
Allow KWallet to raise its unlock dialog when the wallet is locked. Turn this off to treat a locked wallet as "no password" instead, so connecting fails quietly rather than popping a dialog. |
debug_logging |
bool |
false |
Log every request, including the ones deliberately declined. Passwords are never logged at any level. |
How it works
The problem
Every saved Wi-Fi profile in NetworkManager marks its password with a
secret flag. psk-flags=0 means NetworkManager stores the password itself, in
/etc/NetworkManager/system-connections, and any client can connect. psk-flags=1
means agent-owned: NetworkManager deliberately keeps no copy and asks a
registered secret agent at connect time. plasma-nm sets that flag on everything
it saves, so a machine that used to run Plasma typically has a large pile of
agent-owned profiles whose passwords live only in KWallet, under a folder called
Network Management, keyed {uuid};802-11-wireless-security.
VPN profiles are worse off still: NetworkManager never stores a VPN secret
itself, so a VPN password is agent-owned whatever the flags say. plasma-nm keeps
those in the same folder, keyed {uuid};vpn.
Noctalia registers its own secret agent, but that agent has no persistent store ā all it can do is prompt. So on a Noctalia session those profiles ask for a password on every connect, even though the password is sitting in the wallet.
The fix
scripts/kwallet-nm-agent.py is a small daemon that registers on the system bus
as a second NetworkManager secret agent, under the identifier
io.github.grassyloki.kwalletSecrets. It implements the three calls
NetworkManager makes on an agent:
- GetSecrets ā looks up
{uuid};<setting>in the wallet folder and returns the keys it finds. On a miss it answers with theNoSecretserror, which is NetworkManager's cue to ask the next agent in line. That is what keeps Noctalia's prompt working as the fallback for a network the wallet has never seen: this plugin adds a path, it does not take one away. - SaveSecrets ā writes the password into the wallet when a profile is added or changed, so a password you type once ends up in the same store the plugin reads from.
- DeleteSecrets ā removes the wallet entry when the profile is deleted, so the wallet does not accumulate orphans.
VPN secrets are shaped differently
Two things about the vpn setting do not look like any other setting, and the
plugin special-cases both.
On the NetworkManager side, a reply for vpn nests its secrets one level
deeper: {"vpn": {"secrets": {"password": "..."}}}, where the inner map is
a{ss}, not the a{sv} every other setting uses. That is what libnm itself
emits and what plasma-nm sends, so it is the shape the plugin sends. (Modern
NetworkManager is lenient and will also fold flat top-level string entries into
the VPN secrets, but the nested form is the documented one.) The same asymmetry
applies when NetworkManager hands a connection back on a save: the vpn
setting arrives split into data and secrets, and only the latter holds
passwords.
On the KWallet side, the entry is not one map key per secret. NetworkManagerQt
flattens the entire VPN secret map into a single VpnSecrets key whose value is
key, separator, value, separator, key, ⦠joined by the literal %SEP%.
The plugin packs and unpacks that format, so --check still lists real key
names and a password saved here is one plasma-nm can read.
Which secrets a VPN uses depends on the VPN plugin ā openvpn has password,
cert-pass and http-proxy-password, vpnc has Xauth password, openconnect
has a cookie ā so unlike Wi-Fi there is no fixed list of key names to filter
against. Whatever non-empty keys the wallet holds are returned, and whatever
non-empty keys NetworkManager sends are saved. NetworkManager passes hints
naming the one secret it is after; the plugin logs them and returns everything
it found anyway, exactly as plasma-nm does, because a VPN plugin routinely needs
a second secret that the hint never mentions.
wireguard rides along under the same setting. NetworkManager's native
WireGuard is not the vpn setting at all ā it is its own setting, with an
agent-owned private-key and an agent-owned preshared-key per peer ā but it
is a VPN to the user, and plasma-nm keys it {uuid};wireguard. Its reply is the
ordinary flat shape for private-key. A peer's preshared key needs one more
step: the wallet keys it peers.<public-key>.preshared-key, but sending it back
under that flat name makes NetworkManager reject the whole answer with
secret not found. It has to travel inside the setting's peers array instead,
next to the public key that says which peer it belongs to, which is what the
plugin builds from the peer list NetworkManager passes in with the request.
Other details
Two details matter for behaviour you will actually notice. When NetworkManager
sets the REQUEST_NEW flag it is telling the agent that the stored password was
just rejected; the plugin answers NoSecrets there rather than handing back the
same wrong password, so a changed Wi-Fi or VPN password produces a prompt
instead of a retry loop. And every KWallet call is given a deadline (15 seconds by default).
kwalletd6 can wedge ā it has been seen stuck in futex_wait, hanging every
open() ā and a stuck wallet has to degrade to "no password" rather than
freezing the connection attempt.
Reading the wallet
KWallet stores these entries as maps, which on the wire are a raw Qt
QDataStream dump of a QMap of strings: a big-endian count, then alternating
keys and values, each a byte length followed by UTF-16 big-endian text. The
helper encodes and decodes that format directly, which is why it needs no KDE or
Qt bindings ā just D-Bus and a main loop.
Supervision
service.luau does no secret handling at all. It starts the helper under
systemd-cat, so the helper's output lands in the journal under the tag
noctalia-kwallet-secrets instead of a private log file, and re-checks every 30
seconds that it is still alive. "Alive" is decided by looking for the helper's
single-instance lock, an abstract unix socket, in /proc/net/unix; the helper
takes that lock at startup and a duplicate copy exits immediately, so no
combination of restarts can end up with two agents fighting over the same
identifier.
A launch is confirmed the same way. The helper is started in the background, so
the launching shell exits successfully whatever happens to it ā a missing
systemd-cat would look exactly like a clean start. Rather than trust that, the
service waits for the lock to appear and only then reports the agent as running;
if it never appears the service says so, logs it, and tries again on the next
check. The four commands the plugin depends on are verified before any of this,
and a missing one disables the plugin with a message naming it rather than
failing quietly.
Notes
Processes spawned. One long-lived
python3process per session. Around it the service runs only short-lived shell commands:grepagainst/proc/net/unixandid -uto check whether the helper is up,setsidplussystemd-catto launch it, andpkillto stop it when a setting changes.grep,id, andsetsidare not declared as dependencies because they come with coreutils and util-linux on every supported system.Network access. None. The helper talks to two D-Bus buses and nothing else: NetworkManager on the system bus, KWallet on the session bus.
Files written. None. The plugin writes no state of its own; the only thing it ever changes is wallet content, and only through KWallet's own API in response to a NetworkManager save or delete.
Passwords are never logged. Log lines record the profile name, its UUID, the setting, and which key names were found ā never a value.
--checkobeys the same rule.This does not migrate anything. Profiles that already store their password in NetworkManager keep doing that. If you would rather stop depending on the wallet for one network,
nmcli connection modify UUID 802-11-wireless-security.psk-flags 0together with the password moves it into NetworkManager's own store.Running it by hand. The helper lives at
~/.local/state/noctalia/plugins/materialized/community/kwallet-secrets/scripts/kwallet-nm-agent.pyonce the plugin is installed, and--helplists every flag. Stop the supervised copy first, since a second instance exits immediately on its lock:cd ~/.local/state/noctalia/plugins/materialized/community/kwallet-secrets pkill -f 'kwallet-nm-agent[.]py' ./scripts/kwallet-nm-agent.py --debugThe service starts its own copy again within 30 seconds of the manual run ending.
Versions
| Version | Plugin API | Updated |
|---|---|---|
| v1.1.0 latest | 28 | Sep 11, 2026 |
Older versions stay installable on a Noctalia release whose plugin API is below the latest version's.