-
Notifications
You must be signed in to change notification settings - Fork 100
Dev Docs User Authentication
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.
| 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 |
%%{ 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
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 inetc/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
loginorlogin_extcallback
Step 2 — FPLoginCont → afp_logincont() (for multi-step UAMs like DHX, DHX2, Kerberos):
- Calls the UAM's
logincontcallback viaafp_uam->u.uam_login.logincont()
Step 3 — On successful authentication → internal login() function:
-
Denies root login (
pwd->pw_uid == 0returnsAFPERR_NOTAUTH) - Checks connection limit (
cnx_cnt > cnx_max) - Calls
set_groups()to set supplementary groups - Checks
admingidfor admin group membership → callsad_setfuid(0)if admin - Otherwise sets
setegid(pwd->pw_gid)andseteuid(pwd->pw_uid) - Handles
force_user/force_groupoverrides - Calls
set_auth_switch()to enable post-auth AFP commands (ACLs, extended attributes, etc.) - Sends
IPC_LOGINDONEto 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_OUTflag - Fires FCE logout event
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:
- Opens the shared library via
mod_open()(wrapsdlopen) - Finds the exported symbol by stripping the
.soextension — e.g.,uams_dhx2.so→ looks up symboluams_dhx2 - Verifies
uam_type == UAM_MODULE_SERVER(value 1) - Calls the module's
uam_setup()function, which registers its authentication callbacks
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
};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.
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 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 |
| 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 |
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.
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);Source: etc/uams/uams_pam.c
Registered names: "Cleartxt Passwrd" (login + changepw), "ClearTxtUAM" (print auth)
Export symbols: uams_clrtxt, uams_pam
Security: PASSWDLEN)
Backend: PAM (pam_start("netatalk", ...))
Supports login, login_ext, password change, and printer authentication. The PAM flow is:
pam_start("netatalk", username, &PAM_conversation, &pamh)-
pam_set_item(PAM_TTY, "afpd")+pam_set_item(PAM_RHOST, hostname) pam_authenticate(pamh, 0)-
pam_acct_mgmt(pamh, 0)— checks account expiry pam_setcred(pamh, PAM_CRED_ESTABLISH)pam_open_session(pamh, 0)
Password change uses pam_chauthtok() with seteuid(0) when running as root.
Source: etc/uams/uams_passwd.c
Registered names: "Cleartxt Passwrd" (login only, no changepw), "ClearTxtUAM" (print auth)
Export symbols: uams_clrtxt, uams_passwd
Security: crypt() / crypt_checkpass() against /etc/passwd (or /etc/shadow with SHADOWPW)
Does not support password change. Used on systems without PAM.
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_binaryand generatorg = 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
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 viadh_params_generate()usinggcry_prime_generate(), vs fixed 128-bit prime in DHX -
MD5-hashed session key: K is hashed with
GCRY_MD_MD5to 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
adminauthuserfallback vialoginasroot()
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.
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, andk5realmare configured: builds principalservice/fqdn@realmand verifies it in the keytab viakrb5_kt_get_entry() - Otherwise: reads the first entry from the default keytab via
krb5_kt_next_entry() - Stores the principal string in
obj->options.k5principalviaset_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 requestingkGetKerberosSessionKey(type 8) on FPGetSessionToken
No password change support — only UAM_SERVER_LOGIN_EXT is registered.
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):
- Server reads user's 8-byte key from the afppasswd file
- Generates 8-byte random challenge, sends to client with session ID
- Client DES-encrypts the challenge with the user's key, returns it
- Server DES-encrypts its copy of the challenge with the stored key
- 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~/.passwdin user's home directory (withseteuidto 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.
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
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)
- For password change in cleartext UAM: first call returns old password, subsequent calls return new password (tracked by
-
PAM_TEXT_INFO/PAM_BINARY_PROMPT: ignored -
PAM_ERROR_MSG: triggers conversation error
| 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.
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 name -
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.
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.
LDAP integration provides user/group resolution for UUID mapping, used by the ACL system for AFP 3.2+ clients.
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 |
| 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 DirectoryobjectGUID
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).
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()):
- Read existing ACL →
get_nfsv4_acl() - Strip trivial ACEs →
strip_trivial_aces()(removesACE_OWNER,ACE_GROUP,ACE_EVERYONE) - Call
chmod()with the new mode - Read the new ACL (which may have lost explicit ACEs depending on OS version)
- Strip non-trivial ACEs from the new ACL →
strip_nontrivial_aces() - Merge the saved explicit ACEs with the new trivial ACEs →
concat_aces() - Set the merged ACL on the object
POSIX ACL chmod (posix_chmod()):
- Handles the POSIX 1003.1e quirk where
chmod()modifiesACL_MASKinstead ofACL_GROUP_OBJon filesystems with extended ACLs - After
chmod(), findsACL_GROUP_OBJandACL_MASKentries - Updates
ACL_GROUP_OBJpermissions 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
Source: bin/afppasswd/afppasswd.c
The afppasswd utility manages the password file used by the Randnum UAM modules.
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
# 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 |
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
.keyfile exists (convert_passwd()) - Uses file locking (
fcntl F_SETLKW) for atomic writes - Optional cracklib validation via
FascistCheck()
| 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 | 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 |
-
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
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.
- 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
Resources
- Getting Started
- FAQ
- Troubleshooting
- Connect to AFP Server
- Webmin Module
- Benchmarks
- Interoperability with Samba
OS Specific Guides
- Installing Netatalk on Alpine Linux
- Installing Netatalk on Debian Linux
- Installing Netatalk on Fedora Linux
- Installing Netatalk on FreeBSD
- Installing Netatalk on macOS
- Installing Netatalk on NetBSD
- Installing Netatalk on OmniOS
- Installing Netatalk on OpenBSD
- Installing Netatalk on OpenIndiana
- Installing Netatalk on openSUSE
- Installing Netatalk on Raspberry Pi OS
- Installing Netatalk on Solaris
- Installing Netatalk on Ubuntu
Tech Notes
- Capturing AFP network traffic
- Kerberos
- Special Files and Folders
- Spotlight
- MySQL CNID Backend
- Slow AFP read performance
- Limiting Time Machine volumes
- Netatalk and ZFS nbmand property
Retro AFP
Development