Troubleshooting · Kamailio 5.7 / 5.8

Kamailio REGISTER stuck in a 401 Unauthorized loop

The first 401 is normal: it's the digest challenge. A second 401 in reply to a REGISTER that carries credentials means auth_check() rejected them, and the stock config won't tell you why.

Production down right now? Call +44 161 820 1210. We answer 24/7.

What you're seeing

The phone shows "registration failed" or retries every few seconds. In sngrep the same pattern repeats:

sngrep -d any port 5060
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 OK

Kamailio's log is silent: the default route[AUTH] just calls auth_challenge() again.

Likely causes

Most common first.

  1. 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.
  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.
  3. 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.
  4. 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:

kamailio.cfg
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:

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

kamailio.cfg
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:

kamailio.cfg
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)
Still stuck?

Get a Kamailio engineer on it now

Emergency Kamailio support is £120/hour, 24/7. You talk to an engineer who has run Kamailio in production since 2005, not a ticket queue, and we'll sign your NDA before you send configs or traces. See pricing, or how our Kamailio support works.

Call us Email