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 / FPLoginExt → afp_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 — FPLoginCont → afp_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 — FPLogout → afp_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:
- Retrieves the guest account name via
UAM_OPTION_GUEST - Calls
getpwnam(guest)to resolve the account - 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_PROMPT_ECHO_ON: returns username (PAM_username)PAM_PROMPT_ECHO_OFF: returns password (PAM_password)- For password change in cleartext UAM: first call returns old password, subsequent calls return new password (tracked by
PAM_chauthtok_modeandPAM_chauthtok_countstatic variables) PAM_TEXT_INFO/PAM_BINARY_PROMPT: ignoredPAM_ERROR_MSG: triggers conversation error
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:
- A keytab containing the service principal — resolved by
krb5_kt_default()(typically/etc/krb5.keytab) - Configuration (optional but recommended):
k5service— service name component (e.g.,afpserver)fqdn— fully qualified domain namek5realm— 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_GETACL → afp_getacl
- AFP_SETACL → afp_setacl
- AFP_ACCESS → afp_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)
- Password field: 16 hex characters representing an 8-byte (
PASSWDLEN) DES key *as first character of password field means the account is disabled- Optional
.keyfile (same path +.keysuffix): contains a DES master key for encrypting stored passwords
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
- Root login is always denied —
login()rejectspwd->pw_uid == 0withAFPERR_NOTAUTH - Admin group — members of
admingidgetad_setfuid(0)but retain their UID for IPC reporting - Shell validation —
uam_checkuser()checks againstgetusershell()whenOPTION_VALID_SHELLCHECKis set - Password expiry — PAM’s
PAM_NEW_AUTHTOK_REQDtriggers a restricted mode where onlyFPChangePasswordandFPLogoutwork - Connection limits —
cnx_maxenforced atlogin() - Force user/group —
force_userandforce_groupoptions override effective UID/GID after authentication - Umask restoration —
umask()is reset after PAM modules run, in case they changed it - 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
- Use DHX2 as the primary UAM for password-based authentication
- Use Kerberos (Client Krb v2) in enterprise environments with existing KDC infrastructure
- Disable cleartext UAMs unless required for legacy clients and used with encrypted transport
- Configure PAM properly with the
netatalkservice name — enforce account lockout, password complexity - Restrict guest access to specific read-only volumes
- Set
valid shell checkto prevent access by system accounts without login shells
Footnotes
This is a mirror of the Netatalk GitHub Wiki
Last updated 2026-04-06