What you're seeing
The phone shows "registration failed" or retries every few seconds. In sngrep the same pattern repeats:
REGISTER sip:sip.example.com SIP/2.0 no credentials
SIP/2.0 401 Unauthorized
WWW-Authenticate: Digest realm="sip.example.com", nonce="Z6f1...a3"
REGISTER sip:sip.example.com SIP/2.0
Authorization: Digest username="1001", realm="sip.example.com",
nonce="Z6f1...a3", uri="sip:sip.example.com", response="5f2c..."
SIP/2.0 401 Unauthorized should have been 200 OKKamailio's log is silent: the default route[AUTH] just calls auth_challenge() again.
Likely causes
Most common first.
- Wrong password, or HA1 hashed with a different realm. Usually the digest response doesn't match: the password is wrong, or you store pre-computed HA1 values (calculate_ha1 disabled) and they were hashed with a realm other than the one Kamailio challenges with. That's common after a domain change or when phones register to an IP address. auth_check() returns -2.
- Auth username doesn't match the From/To user. With flag 1, auth_check() requires the digest username to match the From user (To user for REGISTER). PBX trunks and many handsets register with an auth ID that differs from the extension, so every attempt fails with -8.
- The user isn't found. If the digest username isn't in the subscriber table (a typo, or a row under another domain when use_domain is enabled), auth_check() returns -3 and the phone is challenged again.
- The nonce is rejected by another node, or after a restart. Each Kamailio instance signs nonces with its own secret unless you set one. A phone that gets a nonce from node A and answers node B fails the nonce check: auth_check() returns -1, or -4 if the nonce has also expired. A restart without a fixed secret invalidates every outstanding nonce too.
How to fix
1. Make Kamailio log why auth_check() failed
Save $rc straight away, because the next function call overwrites it:
route[AUTH] {
if (is_method("REGISTER") || from_uri == myself) {
if (!auth_check("$fd", "subscriber", "1")) {
$var(rc) = $rc;
xlog("L_NOTICE", "AUTH failed: rc=$var(rc) method=$rm src=$si:$sp user=$aU realm=$ar from=$fu\n");
auth_challenge("$fd", "0");
exit;
}
# as in the stock config: keep credentials on REGISTER/PUBLISH
if (!is_method("REGISTER|PUBLISH")) {
consume_credentials();
}
}
if (!(uri == myself || from_uri == myself)) {
sl_send_reply("403", "Not relaying");
exit;
}
}The auth_db return codes: -1 generic error, including a nonce that fails verification; -2 wrong password; -3 unknown user; -4 nonce expired; -5 no credentials for this realm; -6 nonce reused; -8 auth user doesn't match From/To. $aU and $ar are the username and realm the phone actually sent. Compare them with the database row.
2. Check the credentials and HA1 in the database
With calculate_ha1 enabled Kamailio hashes the password column itself. With it disabled it trusts ha1, which must equal MD5(username:realm:password) for the exact realm in the challenge:
SELECT username, domain,
ha1 = MD5(CONCAT(username, ':', domain, ':', password)) AS ha1_matches_domain
FROM subscriber WHERE username = '1001';
-- after a domain/realm change, rebuild both hashes
UPDATE subscriber
SET ha1 = MD5(CONCAT(username, ':', domain, ':', password)),
ha1b = MD5(CONCAT(username, '@', domain, ':', domain, ':', password));If phones register to an IP address, $fd (and so the realm) is that IP, not your domain. Either provision the domain on the phones, or challenge and check with one fixed realm that matches how the HA1 values were built.
3. Trunks whose auth user differs from the From user
Keep flag 1 for handsets. It stops one account registering as another. Relax it only for sources you know:
if ($si == "198.51.100.20") {
# PBX trunk: auth ID "trunk01", From user is the DDI
if (!auth_check("$fd", "subscriber", "0")) {
auth_challenge("$fd", "0");
exit;
}
} else {
if (!auth_check("$fd", "subscriber", "1")) {
auth_challenge("$fd", "0");
exit;
}
}
consume_credentials();4. Share the nonce secret across nodes
Set the same secret on every node behind the same DNS name, and leave nonce counting off unless all nodes share state:
loadmodule "auth.so"
loadmodule "auth_db.so"
# identical on every node; long and random (placeholder shown)
modparam("auth", "secret", "REPLACE-with-the-same-32+-char-random-string")
modparam("auth", "nonce_count", 0)
modparam("auth", "one_time_nonce", 0)