This problem was encountered in a mixed Debian / Ubuntu Linux environment, with Kerberos authentication and an OpenLDAP backend. Some clients are laptops, and are using pam_ccreds for credential caching so that users can login when away from the office.
The problem was that users logging in to a laptop would be told that their password had expired, and forced to change it. Logins on other machines did not report an expired password and did not force the user to change it.
Initial investigation focussed around the PAM configuration on the laptops, and how it differed to the server:
Server PAM
common-auth
auth [success=done new_authtok_reqd=done default=ignore] pam_unix.so try_first_pass audit
auth [success=done new_authtok_reqd=done default=ignore] pam_krb5.so minimum_uid=1000 use_first_pass ignore_root forwardable
auth [success=ok new_authtok_reqd=ok ignore=ignore default=bad] pam_ldap.so use_first_pass ignore_authinfo_unavail
auth [success=done new_authtok_reqd=done ignore=ignore default=bad] pam_ldap.so use_first_pass
common-account
account sufficient pam_ldap.so
account required pam_unix.so
Laptop PAM
common-auth
auth [success=5 user_unknown=ignore new_authtok_reqd=ignore default=ignore] pam_unix.so debug nullok_secure try_first_pass
auth [success=3 new_authtok_reqd=3 default=ignore] pam_krb5.so minimum_uid=1000 defer_pwchange use_first_pass
auth [authinfo_unavail=ignore success=2 new_authtok_reqd=2 default=1] pam_ldap.so debug use_first_pass
auth [success=2 default=die] pam_ccreds.so action=validate use_first_pass
auth [default=die] pam_ccreds.so action=update
auth [default=ignore] pam_ccreds.so action=store
auth required pam_permit.so
auth optional pam_cap.so
common-account
account [user_unknown=ignore authinfo_unavail=ignore default=done] pam_unix.so debug broken_shadow
account [user_unknown=ignore authinfo_unavail=ignore default=done] pam_krb5.so minimum_uid=1000
account [user_unknown=ignore authinfo_unavail=ignore default=done] pam_ldap.so debug
account required pam_permit.so
The difficulty is that the laptop needs the pam_unix module before the kerberos and ldap modules, otherwise the whole stack can time out waiting for the network services to respond when the laptop is offline. However, the pam_unix account module is responding that the user's max password age is 0 for users that should be cached. Once a module has flagged the password as expired, the user is forced to change it.
Solution
Contrary to expectation, the solution was not to change the laptop configuration, but to change the access controls on the LDAP server. Reference pages on Kerberos and LDAP recommend LDAP access controls like this:
ldapsearch -x -H ldaps://server.example.com -D cn=config -W -b cn=config "(|(cn=config)(olcDatabase={1}hdb))" olcAccess
Enter LDAP Password:
# extended LDIF
#
# LDAPv3
# base with scope subtree
# filter: (|(cn=config)(olcDatabase={1}hdb))
# requesting: olcAccess
#
# config
dn: cn=config
# {1}hdb, config
dn: olcDatabase={1}hdb,cn=config
olcAccess: {0}to attrs=userPassword,shadowLastChange,krbPrincipalKey by dn="cn=manager,dc=yuike
e,dc=com,dc=hk" write by anonymous auth by self write by * none
olcAccess: {1}to dn.base="" by * read
olcAccess: {2}to * by dn="cn=manager,dc=yuikee,dc=com,dc=hk" write by * read
# search result
search: 2
result: 0 Success
# numResponses: 3
# numEntries: 2
But this does not allow the reading of the shadowLastChange attribute by pam_unix. It works when changed to:
ldapsearch -x -H ldaps://bluewhale.yuikee.com.hk -D cn=config -W -b cn=config "(|(cn=config)(olcDatabase={1}hdb))" olcAccess
Enter LDAP Password:
# extended LDIF
#
# LDAPv3
# base with scope subtree
# filter: (|(cn=config)(olcDatabase={1}hdb))
# requesting: olcAccess
#
# config
dn: cn=config
# {1}hdb, config
dn: olcDatabase={1}hdb,cn=config
olcAccess: {0}to attrs=shadowLastChange by dn="cn=manager,dc=yuikee,dc=com,dc=
hk" write by anonymous read by self write by * read
olcAccess: {1}to attrs=userPassword,krbPrincipalKey by dn="cn=manager,dc=yuike
e,dc=com,dc=hk" write by anonymous auth by self write by * none
olcAccess: {2}to dn.base="" by * read
olcAccess: {3}to * by dn="cn=manager,dc=yuikee,dc=com,dc=hk" write by * read
# search result
search: 2
result: 0 Success
# numResponses: 3
# numEntries: 2
Here, anonymous is permitted to read shadowLastChange.