10 KiB
Swissbit iShield Key 2 Pro / WebAuthn notes
Status as of 2026-07-19.
Current status
- Affected authenticator: Swissbit iShield Key 2 Pro MIFARE
- Device version:
1.1.0 - FIDO applet:
v1.4.0-0-gd69b47b - AAGUID:
7787a482-13e8-4784-8a06-c7ed49a7aaf4 - Supported protocols: U2F, CTAP 2.0, CTAP 2.1
- Relevant reported options:
clientPin: yesalwaysUv: nomakeCredUvNotRqd: yes
- Tested Vaultwarden release:
1.36.0 - Tested Web Vault release:
2026.4.1 - Tested browser: Google Chrome
150.0.7871.124 - Server: Debian 13, Linux x86_64, SQLite, native installation created by the Proxmox VE Community Script
- Vaultwarden serves HTTPS directly; no reverse proxy is used.
With an FIDO2 PIN already configured on the iShield, unmodified Vaultwarden does not show a PIN prompt during registration. The WebAuthn ceremony eventually times out. Registering the key without a configured PIN works. YubiKeys with an existing PIN work correctly in the same environment, and the iShield works on webauthn.io.
The timeout was subsequently reproduced on a clean Debian 13 test instance and narrowed down further:
| Existing WebAuthn credentials | Policy | iShield registration |
|---|---|---|
| none | discouraged |
succeeds without PIN |
| at least one YubiKey | discouraged |
times out |
| at least one YubiKey | preferred |
succeeds with PIN |
The registered YubiKey was physically disconnected during the failing iShield
attempt. Removing only the YubiKey registration makes the iShield work with
discouraged again. This isolates the additional trigger to the non-empty
excludeCredentials list sent by Vaultwarden, rather than USB device contention.
Vaultwarden deliberately changes the registration and authentication policy to
userVerification: discouraged, since WebAuthn is being used as a second factor.
The iShield advertises makeCredUvNotRqd: yes, so the client is allowed to try
credential creation without PIN-based user verification. That path does not
complete in the tested combination.
Changing both the client challenge and server-side ceremony state from
discouraged to preferred fixes the problem. Registration then requests the
PIN, completes after touching the key, and authentication also succeeds in a new
Incognito session.
Upstream issue:
Swissbit support has also been informed. Following maintainer feedback, an
upstream implementation is in progress on the local branch
webauthn-2fa-user-verification-config. It adds the editable server option
WEBAUTHN_2FA_USER_VERIFICATION, with discouraged remaining the default and
preferred enabling the working PIN/UV flow. The option is also exposed by the
existing Vaultwarden admin configuration UI.
Diagnostic source patch
For Vaultwarden 1.36.0, four values in
src/api/core/two_factor/webauthn.rs were changed:
- state["rs"]["policy"] = Value::String("discouraged".to_string());
+ state["rs"]["policy"] = Value::String("preferred".to_string());
- asc.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;
+ asc.user_verification = UserVerificationPolicy::Preferred;
- state["ast"]["policy"] = Value::String("discouraged".to_string());
+ state["ast"]["policy"] = Value::String("preferred".to_string());
- response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;
+ response.public_key.user_verification = UserVerificationPolicy::Preferred;
Both the response sent to the browser and the server-side state must remain consistent. Changing only the response is not the recommended test.
Current LXC binary and rollback
The original 1.36.0 binary was saved as:
/opt/vaultwarden/bin/vaultwarden.original
The original SHA-256 recorded during the test was:
c7d507bb05a30af1ea3974fac53713be747730ce653e9ee26a1b3cd0a65b2c7e
Rollback to that binary:
systemctl stop vaultwarden
install -o root -g root -m 0755 \
/opt/vaultwarden/bin/vaultwarden.original \
/opt/vaultwarden/bin/vaultwarden
rm -f /opt/vaultwarden/bin/.uv-preferred-version
systemctl start vaultwarden
systemctl status vaultwarden --no-pager
An official PVE Community Script update will overwrite the active custom binary. The iShield is expected to fail again after such an update unless upstream has changed the policy or the custom build is reapplied.
Post-update rebuild script
Install the following as /usr/local/sbin/vaultwarden-uv-patch inside the LXC.
It intentionally aborts if the upstream source no longer contains exactly the
four expected policy expressions.
#!/usr/bin/env bash
set -Eeuo pipefail
VW_ROOT="/opt/vaultwarden"
VW_BIN="${VW_ROOT}/bin/vaultwarden"
BUILD_DIR="/tmp/vaultwarden-uv-build"
SOURCE_FILE="src/api/core/two_factor/webauthn.rs"
if [[ $EUID -ne 0 ]]; then
echo "ERROR: This script must be run as root." >&2
exit 1
fi
if [[ ! -x "$VW_BIN" ]]; then
echo "ERROR: Vaultwarden binary not found: $VW_BIN" >&2
exit 1
fi
VERSION="$("$VW_BIN" --version 2>/dev/null |
grep -oE '[0-9]+\.[0-9]+\.[0-9]+' |
head -n1)"
if [[ -z "$VERSION" ]]; then
echo "ERROR: Could not determine the installed Vaultwarden version." >&2
exit 1
fi
PATCHED_MARKER="${VW_ROOT}/bin/.uv-preferred-version"
if [[ -f "$PATCHED_MARKER" ]] &&
[[ "$(<"$PATCHED_MARKER")" == "$VERSION" ]] &&
"$VW_BIN" --version 2>/dev/null | grep -q 'uv-preferred'; then
echo "Vaultwarden $VERSION is already patched."
exit 0
fi
echo "Preparing UV-preferred build for Vaultwarden $VERSION"
command -v git >/dev/null ||
{ echo "ERROR: git is not installed." >&2; exit 1; }
command -v cargo >/dev/null ||
{ echo "ERROR: cargo is not available." >&2; exit 1; }
if [[ "$BUILD_DIR" != "/tmp/vaultwarden-uv-build" ]]; then
echo "ERROR: Unexpected build directory: $BUILD_DIR" >&2
exit 1
fi
rm -rf -- "$BUILD_DIR"
git clone \
--branch "$VERSION" \
--depth 1 \
https://github.com/dani-garcia/vaultwarden.git \
"$BUILD_DIR"
cd "$BUILD_DIR"
if [[ ! -f "$SOURCE_FILE" ]]; then
echo "ERROR: Expected source file is missing: $SOURCE_FILE" >&2
exit 1
fi
STATE_PATTERN='Value::String("discouraged".to_string())'
POLICY_PATTERN='UserVerificationPolicy::Discouraged_DO_NOT_USE'
STATE_COUNT="$(grep -Fc "$STATE_PATTERN" "$SOURCE_FILE" || true)"
POLICY_COUNT="$(grep -Fc "$POLICY_PATTERN" "$SOURCE_FILE" || true)"
if [[ "$STATE_COUNT" -ne 2 || "$POLICY_COUNT" -ne 2 ]]; then
echo "ERROR: Vaultwarden's WebAuthn implementation has changed." >&2
echo "Expected two state policies and two challenge policies." >&2
echo "Found state=$STATE_COUNT, challenge=$POLICY_COUNT." >&2
echo "No binary was changed." >&2
exit 1
fi
sed -i \
's/Value::String("discouraged".to_string())/Value::String("preferred".to_string())/g' \
"$SOURCE_FILE"
sed -i \
's/UserVerificationPolicy::Discouraged_DO_NOT_USE/UserVerificationPolicy::Preferred/g' \
"$SOURCE_FILE"
REMAINING_STATE="$(grep -Fc "$STATE_PATTERN" "$SOURCE_FILE" || true)"
REMAINING_POLICY="$(grep -Fc "$POLICY_PATTERN" "$SOURCE_FILE" || true)"
PREFERRED_STATE="$(grep -Fc 'Value::String("preferred".to_string())' "$SOURCE_FILE" || true)"
PREFERRED_POLICY="$(grep -Fc 'UserVerificationPolicy::Preferred' "$SOURCE_FILE" || true)"
if [[ "$REMAINING_STATE" -ne 0 ||
"$REMAINING_POLICY" -ne 0 ||
"$PREFERRED_STATE" -lt 2 ||
"$PREFERRED_POLICY" -lt 2 ]]; then
echo "ERROR: Patch verification failed. No binary was changed." >&2
exit 1
fi
git diff --check
echo "Applied source patch:"
git diff -- "$SOURCE_FILE"
export VW_VERSION="${VERSION}-uv-preferred"
cargo build \
--locked \
--features "sqlite,mysql,postgresql" \
--release
TEST_BIN="${VW_ROOT}/bin/vaultwarden.uv-preferred-${VERSION}"
OFFICIAL_BACKUP="${VW_ROOT}/bin/vaultwarden.official-${VERSION}"
install -o root -g root -m 0755 \
target/release/vaultwarden \
"$TEST_BIN"
if ldd "$TEST_BIN" | grep -q 'not found'; then
echo "ERROR: The new binary has unresolved libraries." >&2
rm -f -- "$TEST_BIN"
exit 1
fi
"$TEST_BIN" --version
if [[ ! -e "$OFFICIAL_BACKUP" ]]; then
install -o root -g root -m 0755 \
"$VW_BIN" \
"$OFFICIAL_BACKUP"
fi
echo "Installing patched binary..."
systemctl stop vaultwarden
install -o root -g root -m 0755 \
"$TEST_BIN" \
"$VW_BIN"
printf '%s\n' "$VERSION" >"$PATCHED_MARKER"
if ! systemctl start vaultwarden; then
echo "ERROR: Patched Vaultwarden failed to start; restoring official binary." >&2
install -o root -g root -m 0755 \
"$OFFICIAL_BACKUP" \
"$VW_BIN"
rm -f -- "$PATCHED_MARKER"
systemctl start vaultwarden
exit 1
fi
if ! systemctl is-active --quiet vaultwarden; then
echo "ERROR: Service is not active; restoring official binary." >&2
systemctl stop vaultwarden || true
install -o root -g root -m 0755 \
"$OFFICIAL_BACKUP" \
"$VW_BIN"
rm -f -- "$PATCHED_MARKER"
systemctl start vaultwarden
exit 1
fi
echo
echo "Successfully installed:"
"$VW_BIN" --version
systemctl status vaultwarden --no-pager
echo
echo "Official binary backup: $OFFICIAL_BACKUP"
Make it executable:
chmod 0755 /usr/local/sbin/vaultwarden-uv-patch
Run it after the normal PVE Community Script update:
vaultwarden-uv-patch
Then verify:
/opt/vaultwarden/bin/vaultwarden --version
systemctl status vaultwarden --no-pager
journalctl -u vaultwarden -n 50 --no-pager
The version should contain a suffix similar to:
Vaultwarden 1.37.0-uv-preferred
Version-specific rollback after using the script
For example, to restore the official 1.36.0 binary saved by the script:
systemctl stop vaultwarden
install -o root -g root -m 0755 \
/opt/vaultwarden/bin/vaultwarden.official-1.36.0 \
/opt/vaultwarden/bin/vaultwarden
rm -f /opt/vaultwarden/bin/.uv-preferred-version
systemctl start vaultwarden
systemctl status vaultwarden --no-pager
Before running the script against a future release, first check issue #7437 and the release notes. Do not apply the custom build if Vaultwarden has introduced an official setting or otherwise changed the relevant WebAuthn behavior.