2022-03-11
As mentioned in a previous post, I've spent the last couple of months interfacing with and building a client for Linux PAM.
There are so many great resources and tutorials out there, so I wanted to make this post solely about the developer experience.
If you're unfamiliar with PAM, I suggest reading this.
Components
The following directories are key to navigating PAM config:
/etc/security/
contains system-specific configuration for variables offered by authentication methods./etc/pam.d/
contains configuration for system authentication schemes./usr/lib64/security/
the home of PAM shared objects.
Logging is system-specific, and not all OSs write to /var/log/secure
. You should write to syslog(3) with facility-type LOG_AUTHPRIV
in order to ensure authorization messages are logged to a file that has more restricted permissions.
Documentation
This was the most welcoming surprise for me. Official docs can be found in a couple of places, but the two that I found most useful were the Module Writers' Guide and reading the libpam header files in the /usr/include/security/
directory.
The source code is well documented and in most cases concise.
Gotchas
I won't lie - there are a few.
Configuration
Because PAM is inherently tied to authentication, it can be incredibly easy to lock yourself out of your system if you don't take precautions. I recommend the following 3:
ALWAYS have a backup shell logged in as root while developing. If you have a bug in your PAM client or an incorrect workflow, you take the risk of completely locking yourself out.
Although it might be tempting, try not to test your client on sudo where possible. I promise you'll regret this decision one day!
Test your client on an ephemeral system. Use a VM, remote server, EC2 instance, literally anything that can be torn down and started up again.
Threads
You might consider making your client asynchronous, but due to the nature of certain programs, forking may occur and this really isn't handled well by libpam due to the nature of it's workflows.
For example, sshd likes to do a lot of forking. If your client starts a bunch of threads, then ends up being forked, only one thread is running in the child, and this could leave your system in an unrecoverable state.
After talking with colleagues and seeing a proof-of-concept in Go, we noticed that due to the multi-threaded Go runtime, we saw several occurrences of deadlocks when observing with tools such as strace and gdb.
Raw Pointers
I touched on this in a previous post, but it's worth noting that the underlying values you receive from libpam are all raw pointers. Operating on them isn't safe and if you're not careful you can find yourself dealing with segfaults in trivial code.
Create safe wrappers around these values and copy them into owned data where possible. This gives you some safety and assurances of the lifetime of the data.
pam_exec(8)
Unless you just need to display a message, or update a file, you don't want to use it. It can be simple to get something up and running, but you don't get access to the full libpam experience.
For example, you won't be able to use the pam_conv
structure which provides an easy interface to a user conversation flow. You also won't be able to use the provided auth data handlers that can be used for token management and session caching, etc.
Conclusion
Working with PAM has made for one of the most interesting challenges I've had to date. I'm looking forward to hopefully open-sourcing the code in the near future and probably fixing several bugs.
If you managed to read this far, use Rust.