netatalk.io

Dev Docs User Authentication

Authentication System (UAMs)

1. Authentication Overview

Netatalk’s authentication system is built around User Authentication Modules (UAMs) — dynamically loaded shared libraries that provide pluggable authentication mechanisms for AFP clients. The architecture separates the AFP protocol layer from the authentication method, allowing multiple methods to coexist and be selected at runtime.

Key Source Files

Component File
UAM API include/atalk/uam.h
Authentication flow etc/afpd/auth.c
Auth declarations etc/afpd/auth.h
UAM module loading etc/afpd/uam.c
UAM internal header etc/afpd/uam_auth.h
Guest UAM etc/uams/uams_guest.c
Cleartext PAM UAM etc/uams/uams_pam.c
Cleartext passwd UAM etc/uams/uams_passwd.c
DHX PAM UAM etc/uams/uams_dhx_pam.c
DHX passwd UAM etc/uams/uams_dhx_passwd.c
DHX2 PAM UAM etc/uams/uams_dhx2_pam.c
DHX2 passwd UAM etc/uams/uams_dhx2_passwd.c
Kerberos/GSSAPI UAM etc/uams/uams_gss.c
Random number UAM etc/uams/uams_randnum.c
LDAP config include/atalk/ldapconfig.h, libatalk/acl/ldap_config.c
LDAP operations libatalk/acl/ldap.c
UUID mapping include/atalk/uuid.h, libatalk/acl/uuid.c
ACL system include/atalk/acl.h, libatalk/acl/unix.c
afppasswd utility bin/afppasswd/afppasswd.c

Authentication Architecture

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
graph TB
    subgraph "AFP Client"
        C1["FPLogin /<br/>FPLoginExt"]:::blue
        C2["FPLoginCont"]:::blue
        C3["FPChangePassword"]:::blue
        C4["FPLogout"]:::blue
    end

    subgraph "AFP Daemon — auth.c"
        A1["afp_login()<br/>afp_login_ext()"]:::green
        A2["afp_logincont()"]:::green
        A3["afp_changepw()"]:::green
        A4["afp_logout()"]:::green
        A5["auth_uamfind()"]:::green
        A6["login()<br/>set groups, check admin,<br/>seteuid/setegid, IPC"]:::green
    end

    subgraph "UAM Framework — uam.c"
        U1["uam_load()<br/>dlopen module"]:::yellow
        U2["uam_register()<br/>fill uam_obj callbacks"]:::yellow
        U3["uam_afpserver_option()<br/>UAM_OPTION_* dispatch"]:::yellow
        U4["uam_getname()<br/>uam_checkuser()"]:::yellow
    end

    subgraph "UAM Modules — etc/uams/"
        M1["uams_guest.c<br/>No User Authent"]:::purple
        M2["uams_pam.c<br/>Cleartxt Passwrd"]:::purple
        M3["uams_dhx_pam.c<br/>DHCAST128"]:::purple
        M4["uams_dhx2_pam.c<br/>DHX2"]:::purple
        M5["uams_gss.c<br/>Client Krb v2"]:::purple
        M6["uams_randnum.c<br/>Randnum exchange"]:::purple
    end

    subgraph "Auth Backends"
        B1["PAM<br/>pam_start netatalk"]:::salmon
        B2["Unix passwd<br/>shadow"]:::salmon
        B3["Kerberos<br/>KDC / keytab"]:::salmon
        B4["afppasswd<br/>file"]:::salmon
    end

    C1 --> A1
    C2 --> A2
    C3 --> A3
    C4 --> A4
    A1 --> A5
    A5 --> U2
    A1 --> A6
    A2 --> A6
    U1 --> M1 & M2 & M3 & M4 & M5 & M6
    M1 --> U3
    M2 --> B1
    M3 --> B1
    M4 --> B1
    M5 --> B3
    M6 --> B4
    M2 -.->|"passwd variant"| B2
    M3 -.->|"passwd variant"| B2

    classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
    classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
    classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
    classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
    classDef salmon fill:#fab1a0,stroke:#333,rx:10,ry:10
    classDef cyan fill:#81ecec,stroke:#333,rx:10,ry:10

2. AFP Login Flow

When a Mac client connects, the AFP protocol carries the authentication through these steps:

Step 1 — FPLogin / FPLoginExtafp_login() / afp_login_ext() in etc/afpd/auth.c: - Parses the AFP version string from the afp_versions[] table in etc/afpd/auth.h (AFP 1.1 through AFP 3.4) - Extracts the UAM name from the request - Calls auth_uamfind(UAM_SERVER_LOGIN, ...) to locate the matching UAM in the linked list - Calls create_session_key() — generates a 64-byte random key (SESSIONKEY_LEN) - Invokes the UAM’s login or login_ext callback

Step 2 — FPLoginContafp_logincont() (for multi-step UAMs like DHX, DHX2, Kerberos): - Calls the UAM’s logincont callback via afp_uam->u.uam_login.logincont()

Step 3 — On successful authentication → internal login() function: - Denies root login (pwd->pw_uid == 0 returns AFPERR_NOTAUTH) - Checks connection limit (cnx_cnt > cnx_max) - Calls set_groups() to set supplementary groups - Checks admingid for admin group membership → calls ad_setfuid(0) if admin - Otherwise sets setegid(pwd->pw_gid) and seteuid(pwd->pw_uid) - Handles force_user / force_group overrides - Calls set_auth_switch() to enable post-auth AFP commands (ACLs, extended attributes, etc.) - Sends IPC_LOGINDONE to parent process - Fires FCE login event

Step 4 — FPLogoutafp_logout(): - Closes all open forks (of_close_all_forks), all volumes (close_all_vol) - Sets DSI_AFP_LOGGED_OUT flag - Fires FCE logout event


3. UAM Framework

Module Loading

UAM modules are loaded at startup by auth_load() in etc/afpd/auth.c, which tokenizes the configured UAM list and calls uam_load() in etc/afpd/uam.c for each module:

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
sequenceDiagram
    participant afpd as afpd startup
    participant auth as auth_load()
    participant uam as uam_load()
    participant mod as UAM .so module

    afpd->>auth: auth_load(obj, path, "uams_dhx2.so uams_guest.so ...")
    loop for each module name
        auth->>uam: uam_load(obj, "/path/uams_dhx2.so", "uams_dhx2.so")
        uam->>mod: mod_open() → dlopen()
        mod-->>uam: handle
        uam->>mod: mod_symbol(handle, "uams_dhx2")
        Note right of mod: strips .so to find symbol name
        mod-->>uam: struct uam_export *
        uam->>uam: verify uam_type == UAM_MODULE_SERVER
        uam->>mod: uam_fcn->uam_setup(obj, name)
        Note right of mod: uam_setup calls uam_register()<br/>for each auth type
        mod-->>uam: 0 (success)
        uam-->>auth: struct uam_mod *
        auth->>auth: uam_attach(&uam_modules, mod)
    end

The uam_load() function: 1. Opens the shared library via mod_open() (wraps dlopen) 2. Finds the exported symbol by stripping the .so extension — e.g., uams_dhx2.so → looks up symbol uams_dhx2 3. Verifies uam_type == UAM_MODULE_SERVER (value 1) 4. Calls the module’s uam_setup() function, which registers its authentication callbacks

struct uam_export

Every UAM module exports a struct uam_export defined in include/atalk/uam.h:

struct uam_export {
    int uam_type, uam_version;        // UAM_MODULE_SERVER (1), UAM_MODULE_VERSION (1)
    int (*uam_setup)(void *, const char *);  // called on module load
    void (*uam_cleanup)(void);               // called on module unload
};

struct uam_obj

Each registered authentication method is tracked as a struct uam_obj (defined in etc/afpd/uam_auth.h) in a doubly-linked list:

struct uam_obj {
    const char *uam_name;    // authentication method name (e.g., "DHX2")
    char *uam_path;          // shared library path
    int uam_count;           // reference count
    union {
        struct {
            int (*login)(...);
            int (*logincont)(...);
            void (*logout)(void);
            int (*login_ext)(...);
        } uam_login;
        int (*uam_changepw)(...);
    } u;
    struct uam_obj *uam_prev, *uam_next;
};

Two separate linked lists exist: uam_login for login UAMs and uam_changepw for password-change UAMs, selected by the UAM_LIST() macro in etc/afpd/auth.c.

Registration API

uam_register() in etc/afpd/uam.c is a variadic function that accepts different numbers of callback arguments depending on the registration type:

Type Constant Arguments
UAM_SERVER_LOGIN 1 << 0 login, logincont, logout (3 callbacks)
UAM_SERVER_LOGIN_EXT 1 << 3 login, logincont, logout, login_ext (4 callbacks)
UAM_SERVER_CHANGEPW 1 << 1 changepw (1 callback)
UAM_SERVER_PRINTAUTH 1 << 2 printer-auth callback

UAM Option Flags

UAM modules retrieve server state through uam_afpserver_option() in etc/afpd/uam.c, using constants from include/atalk/uam.h:

Constant Value Description
UAM_OPTION_USERNAME 1 << 0 Pointer to username buffer
UAM_OPTION_GUEST 1 << 1 Configured guest account name
UAM_OPTION_PASSWDOPT 1 << 2 Password file path/options
UAM_OPTION_SIGNATURE 1 << 3 16-byte server signature
UAM_OPTION_RANDNUM 1 << 4 Generate random number
UAM_OPTION_HOSTNAME 1 << 5 Server hostname
UAM_OPTION_COOKIE 1 << 6 Per-UAM cookie storage
UAM_OPTION_PROTOCOL 1 << 7 DSI or ASP protocol
UAM_OPTION_CLIENTNAME 1 << 8 Client IP / hostname
UAM_OPTION_KRB5SERVICE 1 << 9 Kerberos service name
UAM_OPTION_MACCHARSET 1 << 10 Mac charset handle
UAM_OPTION_UNIXCHARSET 1 << 11 Unix charset handle
UAM_OPTION_SESSIONINFO 1 << 12 struct session_info pointer
UAM_OPTION_KRB5REALM 1 << 13 Kerberos realm
UAM_OPTION_FQDN 1 << 14 Fully qualified domain name

Password sub-options (via UAM_OPTION_PASSWDOPT with length parameter):

Constant Value Description
UAM_PASSWD_FILENAME 1 << 0 Path to password file
UAM_PASSWD_MINLENGTH 1 << 1 Minimum password length
UAM_PASSWD_EXPIRETIME 1 << 3 Not implemented

Helper Functions

Function File Purpose
uam_getname() etc/afpd/uam.c Resolves username: tries getpwnam(), then NT/AD domain prefixed, then case-insensitive UCS2 matching
uam_checkuser() etc/afpd/uam.c Validates user has a valid shell via getusershell() when OPTION_VALID_SHELLCHECK is set
uam_random_string() etc/afpd/uam.c Reads from /dev/urandom, falls back to random() with time-based seed

Session Management

struct session_info in include/atalk/uam.h tracks per-connection cryptographic state:

#define SESSIONKEY_LEN  64
#define SESSIONTOKEN_LEN 8

struct session_info {
    void    *sessionkey;       // 64-byte random session key
    size_t  sessionkey_len;
    void    *cryptedkey;       // Kerberos/GSSAPI wrapped key
    size_t  cryptedkey_len;
    void    *sessiontoken;     // 8-byte token (PID-based) for FPGetSessionToken
    size_t  sessiontoken_len;
    void    *clientid;         // Client ID buffer (idlen + id + boottime)
    size_t  clientid_len;
};

The session token is created by create_session_token() using the process PID. The session key is created by create_session_key() using uam_random_string(). Both functions are in etc/afpd/auth.c.


4. UAM Modules

Guest — “No User Authent”

Source: etc/uams/uams_guest.c Registered names: "No User Authent" (login), "NoAuthUAM" (print auth) Export symbol: uams_guest Security: None — anonymous access

The guest UAM provides anonymous access mapped to a configured system account:

  1. Retrieves the guest account name via UAM_OPTION_GUEST
  2. Calls getpwnam(guest) to resolve the account
  3. Returns immediately — no password required

Registration in uam_setup():

uam_register(UAM_SERVER_LOGIN_EXT, path, "No User Authent",
             noauth_login, NULL, NULL, noauth_login_ext);
uam_register(UAM_SERVER_PRINTAUTH, path, "NoAuthUAM", noauth_printer);

Cleartext Password — PAM variant

Source: etc/uams/uams_pam.c Registered names: "Cleartxt Passwrd" (login + changepw), "ClearTxtUAM" (print auth) Export symbols: uams_clrtxt, uams_pam Security: ⚠️ Password sent in cleartext — max 8 bytes (PASSWDLEN) Backend: PAM (pam_start("netatalk", ...))

Supports login, login_ext, password change, and printer authentication. The PAM flow is: 1. pam_start("netatalk", username, &PAM_conversation, &pamh) 2. pam_set_item(PAM_TTY, "afpd") + pam_set_item(PAM_RHOST, hostname) 3. pam_authenticate(pamh, 0) 4. pam_acct_mgmt(pamh, 0) — checks account expiry 5. pam_setcred(pamh, PAM_CRED_ESTABLISH) 6. pam_open_session(pamh, 0)

Password change uses pam_chauthtok() with seteuid(0) when running as root.

Cleartext Password — passwd variant

Source: etc/uams/uams_passwd.c Registered names: "Cleartxt Passwrd" (login only, no changepw), "ClearTxtUAM" (print auth) Export symbols: uams_clrtxt, uams_passwd Security: ⚠️ Password sent in cleartext — max 8 bytes Backend: crypt() / crypt_checkpass() against /etc/passwd (or /etc/shadow with SHADOWPW)

Does not support password change. Used on systems without PAM.

DHX — DHCAST128 (Diffie-Hellman Exchange)

Sources: etc/uams/uams_dhx_pam.c (PAM), etc/uams/uams_dhx_passwd.c (passwd) Registered name: "DHCAST128" Export symbols: uams_dhx, uams_dhx_pam, uams_dhx_passwd Security: Password encrypted during transit with CAST-128 Crypto library: libgcrypt

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
sequenceDiagram
    participant C as AFP Client
    participant S as afpd DHX UAM
    participant B as Auth Backend

    C->>S: FPLogin("DHCAST128", username, Ma)
    Note right of S: Fixed 128-bit prime p,<br/>g = 7, random Rb<br/>Mb = g^Rb mod p<br/>K = Ma^Rb mod p
    S->>S: Random challenge (16 bytes)
    S->>S: CAST5-CBC encrypt(K, IV="CJalbert")
    S-->>C: AFPERR_AUTHCONT + [sessid ∥ Mb ∥ encrypted]

    C->>C: K = Mb^Ra mod p, decrypt, verify
    C->>C: CAST5-CBC encrypt(K, IV="LWallace")
    C->>S: FPLoginCont(sessid, [challenge+1 ∥ password])

    S->>S: Decrypt, verify challenge+1
    S->>B: Authenticate password
    B-->>S: result
    S-->>C: AFP_OK / AFPERR_NOTAUTH

Protocol details: - Fixed 128-bit prime p_binary and generator g = 7 (g_binary) defined in the source - Cipher: CAST5-CBC (GCRY_CIPHER_CAST5) - IVs: server→client "CJalbert" (msg2_iv), client→server "LWallace" (msg3_iv) - Session ID: hash of obj pointer via dhxhash() macro - Password buffer: 64 bytes (PASSWDLEN) - PAM variant supports password change; passwd variant does not

DHX2 — Enhanced Diffie-Hellman Exchange

Sources: etc/uams/uams_dhx2_pam.c (PAM), etc/uams/uams_dhx2_passwd.c (passwd) Registered name: "DHX2" Export symbols: uams_dhx2, uams_dhx2_pam, uams_dhx2_passwd Security: Stronger than DHX — 1024-bit generated primes, nonce-based replay prevention Crypto library: libgcrypt

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
sequenceDiagram
    participant C as AFP Client
    participant S as afpd DHX2 UAM
    participant B as Auth Backend

    C->>S: FPLogin("DHX2", username)
    Note right of S: Generate 1024-bit prime p,<br/>generator g, random Ra<br/>Ma = g^Ra mod p
    S-->>C: AFPERR_AUTHCONT + [ID ∥ g ∥ len ∥ p ∥ Ma]

    C->>C: Mb = g^Rb mod p, K = Ma^Rb mod p<br/>K_hash = MD5(K), encrypt clientNonce
    C->>S: FPLoginCont[ID ∥ Mb ∥ encrypted(clientNonce)]

    S->>S: K = Mb^Ra mod p, K_hash = MD5(K)
    S->>S: Decrypt clientNonce, generate serverNonce
    S->>S: Encrypt [clientNonce+1 ∥ serverNonce]
    S-->>C: AFPERR_AUTHCONT + [ID+1 ∥ encrypted block]

    C->>C: Decrypt, verify clientNonce+1
    C->>C: Encrypt [serverNonce+1 ∥ password (256 bytes)]
    C->>S: FPLoginCont[ID+1 ∥ encrypted(serverNonce+1 ∥ password)]

    S->>S: Decrypt, verify serverNonce+1
    S->>B: Authenticate password
    B-->>S: result
    S-->>C: AFP_OK / AFPERR_NOTAUTH

Key improvements over DHX: - 1024-bit generated primes (PRIMEBITS = 1024) generated at startup via dh_params_generate() using gcry_prime_generate(), vs fixed 128-bit prime in DHX - MD5-hashed session key: K is hashed with GCRY_MD_MD5 to derive the CAST5 encryption key - Nonce exchange: Client and server exchange nonces that must be returned incremented, preventing replay attacks - 256-byte password buffer: Supports much longer passwords than DHX’s 64 bytes - 3-step authentication: login → logincont1 (nonce exchange) → logincont2 (password verify), tracked by session ID and ID+1 - Charset conversion: Password converted from Mac encoding to Unix encoding (convert_string_allocate(CH_MAC, CH_UNIX, ...)) in PAM variant - Admin auth user: Supports adminauthuser fallback via loginasroot()

IVs (same names, swapped usage): - Client→server: "LWallace" (dhx_c2siv) - Server→client: "CJalbert" (dhx_s2civ)

Password change uses a 3-step state machine (dhx2_changepw()): changepw_1 → changepw_2 → changepw_3, with old and new passwords (256 bytes each) transmitted in the final step.

Kerberos / GSSAPI — “Client Krb v2”

Source: etc/uams/uams_gss.c Registered name: "Client Krb v2" Export symbol: uams_gss Security: Ticket-based — no passwords transmitted Libraries: GSSAPI, optionally krb5

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
sequenceDiagram
    participant C as AFP Client
    participant S as afpd GSS UAM
    participant K as KDC / keytab

    C->>S: FPLogin("Client Krb v2")
    S-->>C: AFPERR_AUTHCONT + [session_id = 1]

    C->>C: Obtain service ticket from KDC
    C->>S: FPLoginCont(pad, id, username, ticket_len, ticket)

    S->>S: gss_accept_sec_context()
    S->>S: get_client_username():<br/>gss_display_name → strip realm/instance
    S->>S: wrap_sessionkey():<br/>gss_wrap(sessionkey) → cryptedkey
    S->>S: uam_getname() + uam_checkuser()
    S-->>C: AFP_OK + [auth_len ∥ authenticator]

Service principal setup (gss_create_principal() in etc/uams/uams_gss.c): - If k5service, fqdn, and k5realm are configured: builds principal service/fqdn@realm and verifies it in the keytab via krb5_kt_get_entry() - Otherwise: reads the first entry from the default keytab via krb5_kt_next_entry() - Stores the principal string in obj->options.k5principal via set_principal() (max 255 bytes)

Username extraction (get_client_username()): - Calls gss_display_name() on the client name - Strips the realm (@REALM) and instance (/instance) - Copies the bare username into afpd’s buffer

Session key wrapping (wrap_sessionkey()): - Wraps afpd’s 64-byte session key with gss_wrap() using confidentiality + integrity - Stored in sinfo->cryptedkey — returned to clients requesting kGetKerberosSessionKey (type 8) on FPGetSessionToken

No password change support — only UAM_SERVER_LOGIN_EXT is registered.

Randnum — Random Number Exchange

Source: etc/uams/uams_randnum.c Registered names: "Randnum exchange", "2-Way Randnum exchange" (login), "Randnum Exchange" (changepw) Export symbol: uams_randnum Security: DES-based challenge/response using afppasswd file Crypto library: libgcrypt (DES/ECB)

Protocol flow (Randnum exchange): 1. Server reads user’s 8-byte key from the afppasswd file 2. Generates 8-byte random challenge, sends to client with session ID 3. Client DES-encrypts the challenge with the user’s key, returns it 4. Server DES-encrypts its copy of the challenge with the stored key 5. Compares the two — match means authentication succeeds

2-Way Randnum exchange (rand2num_logincont()): - Each byte of the DES key is shifted left one bit before encryption - Client also sends its own 8-byte challenge; server encrypts it and returns the result — mutual authentication

Password file access (randpass()): - If path starts with ~: reads ~/.passwd in user’s home directory (with seteuid to user) - Otherwise: reads the global afppasswd file (with seteuid(0))

afppasswd file format (afppasswd() function):

username:hex_password:last_login_date:failed_count

Where hex_password is the DES key in hex. An optional .key file provides a DES master key for encrypting stored passwords.


5. PAM Integration

All PAM-based UAMs (cleartext, DHX, DHX2) use the same integration pattern with PAM service name "netatalk":

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
graph TB
    subgraph "PAM Login Sequence"
        P1["pam_start('netatalk',<br/>username, &conv, &pamh)"]:::blue
        P2["pam_set_item<br/>(PAM_TTY, 'afpd')"]:::blue
        P3["pam_set_item<br/>(PAM_RHOST, hostname)"]:::blue
        P4["pam_authenticate<br/>(pamh, 0)"]:::green
        P5["pam_acct_mgmt<br/>(pamh, 0)"]:::green
        P6["pam_setcred<br/>(pamh, PAM_CRED_ESTABLISH)"]:::green
        P7["pam_open_session<br/>(pamh, 0)"]:::green
    end

    subgraph "PAM Logout"
        L1["pam_close_session<br/>(pamh, 0)"]:::salmon
        L2["pam_end<br/>(pamh, 0)"]:::salmon
    end

    subgraph "PAM Password Change"
        C1["pam_start('netatalk',<br/>username, ...)"]:::yellow
        C2["pam_authenticate — verify<br/>old password"]:::yellow
        C3["pam_acct_mgmt"]:::yellow
        C4["pam_chauthtok<br/>(as euid 0)"]:::yellow
    end

    P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7
    L1 --> L2
    C1 --> C2 --> C3 --> C4

    classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
    classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
    classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
    classDef salmon fill:#fab1a0,stroke:#333,rx:10,ry:10

PAM Conversation Function

Each PAM UAM module implements a static PAM_conv() function conforming to struct pam_conv:

PAM Error Mapping

PAM Error AFP Error
PAM_MAXTRIES AFPERR_PWDEXPR
PAM_NEW_AUTHTOK_REQD AFPERR_PWDEXPR
PAM_AUTHTOKEN_REQD AFPERR_PWDCHNG
Other failures AFPERR_NOTAUTH

When AFPERR_PWDEXPR is returned, set_auth_switch() in etc/afpd/auth.c puts all AFP commands except FPChangePassword and FPLogout into an error state, allowing the user to change their expired password.


6. Kerberos / GSSAPI Details

Service Principal Requirements

The GSS UAM in etc/uams/uams_gss.c requires:

  1. A keytab containing the service principal — resolved by krb5_kt_default() (typically /etc/krb5.keytab)
  2. Configuration (optional but recommended):
  3. k5service — service name component (e.g., afpserver)
  4. fqdn — fully qualified domain name
  5. k5realm — Kerberos realm name

If all three are configured, the principal is built as k5service/fqdn@k5realm and verified against the keytab. Otherwise, the first entry in the default keytab is used.

Principal Storage

The principal is stored in obj->options.k5principal in a wire format: - 1 byte: number of principals (always 1) - 1 byte: principal name length - N bytes: principal string (null-terminated)

This is advertised to clients in the server’s status response.


7. LDAP Integration

LDAP integration provides user/group resolution for UUID mapping, used by the ACL system for AFP 3.2+ clients.

Configuration

Configured in the [Global] section of afp.conf, parsed by acl_ldap_readconfig() in libatalk/acl/ldap_config.c:

Config Key Variable Required Description
ldap uri ldap_uri Yes LDAP server URI
ldap auth method ldap_auth_method Yes none (anonymous) or simple
ldap auth dn ldap_auth_dn No Bind DN for simple auth
ldap auth pw ldap_auth_pw No Bind password
ldap userbase ldap_userbase Yes Base DN for user searches
ldap groupbase ldap_groupbase Yes Base DN for group searches
ldap uuid attr ldap_uuid_attr Yes UUID attribute name
ldap name attr ldap_name_attr Yes Username attribute name
ldap group attr ldap_group_attr Yes Group name attribute
ldap uuid encoding ldap_uuid_encoding No string or ms-guid
ldap user filter ldap_userfilter No Custom user search filter
ldap group filter ldap_groupfilter No Custom group search filter

Implementation

File Purpose
include/atalk/ldapconfig.h Config struct definitions: struct ldap_pref, struct pref_array, ldap_uuid_encoding_type
libatalk/acl/ldap_config.c Config parsing from iniparser dictionary
libatalk/acl/ldap.c LDAP operations: search with scope, connection management (LDAP_VERSION3)

The LDAP module uses connection keepalive to avoid repeated binds, and supports search scopes: base, one (LDAP_SCOPE_ONELEVEL), sub (LDAP_SCOPE_SUBTREE).

UUID encoding types (ldap_uuid_encoding_type enum): - LDAP_UUID_ENCODING_STRING (0): Plain ASCII UUID string - LDAP_UUID_ENCODING_MSGUID (1): Raw binary, for Active Directory objectGUID


8. ACL System

UUID Mapping

AFP 3.2+ uses UUIDs to identify users and groups for ACL operations.

Header: include/atalk/uuid.h Implementation: libatalk/acl/uuid.c

#define UUID_BINSIZE 16
typedef unsigned char atalk_uuid_t[UUID_BINSIZE];
typedef enum { UUID_USER = 1, UUID_GROUP = 2, UUID_ENOENT = 4 } uuidtype_t;

Interface functions:

Function Description
getuuidfromname() Map username/groupname → UUID (via LDAP or local generation)
getnamefromuuid() Map UUID → name and type
localuuid_from_id() Generate local UUID from Unix UID/GID: user prefix ff:ff:ee:ee:dd:dd:cc:cc:bb:bb:aa:aa + 4-byte ID; group prefix ab:cd:ef:ab:cd:ef:ab:cd:ef:ab:cd:ef + 4-byte ID
uuid_bin2string() Convert 16-byte UUID to dash-delimited hex string
uuid_string2bin() Parse hex UUID string (with dashes) to binary

UUID resolution is used by afp_getuserinfo() in etc/afpd/auth.c when the USERIBIT_UUID flag is set (requires OPTION_UUID server option).

POSIX and NFSv4 ACLs

Header: include/atalk/acl.h Implementation: libatalk/acl/unix.c

The ACL system supports two backends selected at compile time:

Backend Compile Flag chmod Wrapper Platform
NFSv4 ACLs HAVE_NFSV4_ACLS nfsv4_chmod() Solaris/Illumos
POSIX ACLs HAVE_POSIX_ACLS posix_chmod() Linux, FreeBSD

NFSv4 chmod flow (nfsv4_chmod()): 1. Read existing ACL → get_nfsv4_acl() 2. Strip trivial ACEs → strip_trivial_aces() (removes ACE_OWNER, ACE_GROUP, ACE_EVERYONE) 3. Call chmod() with the new mode 4. Read the new ACL (which may have lost explicit ACEs depending on OS version) 5. Strip non-trivial ACEs from the new ACL → strip_nontrivial_aces() 6. Merge the saved explicit ACEs with the new trivial ACEs → concat_aces() 7. Set the merged ACL on the object

POSIX ACL chmod (posix_chmod()): - Handles the POSIX 1003.1e quirk where chmod() modifies ACL_MASK instead of ACL_GROUP_OBJ on filesystems with extended ACLs - After chmod(), finds ACL_GROUP_OBJ and ACL_MASK entries - Updates ACL_GROUP_OBJ permissions to match the requested group mode bits - Recalculates mask via acl_calc_mask()

AFP command handlers for ACLs are enabled in set_auth_switch() in etc/afpd/auth.c for AFP 3.2+: - AFP_GETACLafp_getacl - AFP_SETACLafp_setacl - AFP_ACCESSafp_access


9. Password Management — afppasswd

Source: bin/afppasswd/afppasswd.c

The afppasswd utility manages the password file used by the Randnum UAM modules.

File Format

username:hex_password(16 chars):last_login_date(16 chars):failed_count(8 chars)

Command Line

# Root syntax
afppasswd [-c] [-a] [-f] [-n] [-u minuid] [-p path] [-w string] [username]

# User syntax (no options allowed)
afppasswd
Flag Description
-c Create and initialize password file (root only)
-a Add a new user
-f Force action (overwrite existing file)
-n Disable cracklib checking (if compiled with USE_CRACKLIB)
-u minuid Minimum UID to include when creating file (default: 100)
-p path Path to afppasswd file (default: _PATH_AFPDPWFILE)
-w string Specify password on command line

Operations

Create (create_file() function): - Iterates all system users via getpwent() - Skips UIDs below minuid - Writes each as username:****************:****************:********\n

Update (update_passwd() function): - Searches for the username entry in the file - Non-root users must verify their old password first - Converts password to/from hex using DES encryption if a .key file exists (convert_passwd()) - Uses file locking (fcntl F_SETLKW) for atomic writes - Optional cracklib validation via FascistCheck()


10. Security Considerations

UAM Security Comparison

UAM Password Protection Key Size Password Length Replay Prevention Recommended
No User Authent None N/A N/A No Only for public shares
Cleartxt Passwrd ⚠️ None N/A 8 bytes max No No — use only with encrypted transport
DHCAST128 (DHX) CAST5-CBC 128-bit fixed DH 64 bytes Challenge-based Legacy only
DHX2 CAST5-CBC + MD5 1024-bit generated DH 256 bytes Nonce exchange Yes — recommended
Client Krb v2 Kerberos tickets Per-realm N/A Ticket-based Yes — enterprise
Randnum exchange DES-ECB 56-bit DES 8 bytes Challenge-based Legacy only
2-Way Randnum DES-ECB (shifted) 56-bit DES 8 bytes Mutual challenge Legacy only

Key Security Properties

  1. Root login is always deniedlogin() rejects pwd->pw_uid == 0 with AFPERR_NOTAUTH
  2. Admin group — members of admingid get ad_setfuid(0) but retain their UID for IPC reporting
  3. Shell validationuam_checkuser() checks against getusershell() when OPTION_VALID_SHELLCHECK is set
  4. Password expiry — PAM’s PAM_NEW_AUTHTOK_REQD triggers a restricted mode where only FPChangePassword and FPLogout work
  5. Connection limitscnx_max enforced at login()
  6. Force user/groupforce_user and force_group options override effective UID/GID after authentication
  7. Umask restorationumask() is reset after PAM modules run, in case they changed it
  8. Sensitive data clearing — All UAM modules use explicit_bzero() to wipe passwords from memory after use

Transport Encryption

DHX and DHX2 protect the password during the authentication exchange, but all subsequent AFP traffic is unencrypted. Netatalk’s DSI (Data Stream Interface) does not provide transport-level encryption. For full transport security, tunnel AFP over SSH or use a VPN.

Recommendations

  1. Use DHX2 as the primary UAM for password-based authentication
  2. Use Kerberos (Client Krb v2) in enterprise environments with existing KDC infrastructure
  3. Disable cleartext UAMs unless required for legacy clients and used with encrypted transport
  4. Configure PAM properly with the netatalk service name — enforce account lockout, password complexity
  5. Restrict guest access to specific read-only volumes
  6. Set valid shell check to prevent access by system accounts without login shells

Footnotes

This is a mirror of the Netatalk GitHub Wiki

Last updated 2026-04-06