When making an Internet-connected device that requires a remote connection, you’ll want to ensure no unauthorized actors can use the connection to get into the device. A good password will only get you so far, and public-key authentication a small bit further. The problem with passwords and keys is that if someone manages to intercept them, they need to be rotated, and that is not necessarily a trivial thing to do.
Multi-factor authentication (MFA) with time-based one-time passwords can help in these situations. Rotating the login secrets is easy when it happens automatically every 30 seconds. Authentication codes have their weaknesses which are also discussed in this post, but in general, they are a good addition to the login flow. In this blog post, I’ll show how to integrate Google Authenticator into a Yocto system. As usual, the steps are not Yocto-specific, they apply pretty much to any Linux system.
Multi-Factor Authentication
But first, let’s briefly go through what multi-factor authentication (MFA) is. In MFA, the user needs to provide multiple factors for authentication. Some commonly used components for MFA are:
- Knowledge factor: something that the user knows (password, PIN, etc.)
- Possession factor: something that the user has (smart card, security token generator, etc.)
- Inherent factor: something that the user is (fingerprint, retina, etc.)
- Location factor: some place the user is in (geographic location, IP address, etc.)
If you’re using just a password to log in, you’re using a single-factor authentication with the knowledge factor. If you use both a password and an authenticator code, it is multi-factor authentication because you use multiple factors. Simple as. The commonly seen term two-factor authentication (2FA) is in practice an MFA with exactly two factors, most often knowledge and possession factors.

Combining the factors in different ways allows quite “wild” and “exciting” authentication sequences. In this blog text, we are going to go the typical route, aka. 2FA with a password and an authenticator. However, you could add additional factors into the mix quite easily, or even come up with your own. It is also possible to use the same factor multiple times, like requiring a PIN and a password which are both knowledge factors.
Adding MFA Possession Factor to Yocto Image
To add the MFA functionality to Linux login flow, we will use the Google Authenticator PAM plugin. Since we’re going to be adding a PAM plugin, the first thing we want to enable is the PAM distro feature somewhere in the build configuration (e.g. local.conf
, distro.conf
):
DISTRO_FEATURES:append = " pam"
I have briefly talked about PAM before, but to recap: it allows plugin-based authentication flows in Linux systems. Typically the pam_unix
module is defined for the authentication, and it simply prompts the user for a password. Since the authentication flow is plugin-based and configured in a text file, we can easily add or remove modules. These additional modules can authenticate different factors, effectively creating a multi-factor authentication flow.
Next, we’ll want to install the PAM library and authenticator setup tool. A BitBake-recipe for these is readily available in meta-security. To install the packages, we can add the following two slightly confusingly named packages to the image:
# google-authenticator setup tool
IMAGE_INSTALL:append = " google-authenticator-libpam"
# Google authenticator PAM library
IMAGE_INSTALL:append = " pam-google-authenticator"
Then, we need to add the Google authenticator PAM library to be a part of the user authentication flow. In practice, you’ll want to add auth required pam_google_authenticator.so
somewhere where it is read during the authentication process. You can either patch the relevant line in, or provide your own PAM configuration files.
The correct PAM configuration file depends on what type of logins you’ll want to protect with MFA. For example, if you want to require authentication codes just for the remote SSH connections, /etc/pam.d/sshd
would be a good place. Something like this should work:
auth include common-auth
# Notice this line
auth required pam_google_authenticator.so
account required pam_nologin.so
account include common-account
password include common-password
session optional pam_keyinit.so force revoke
session include common-session
session required pam_loginuid.so
To properly allow logging in with MFA enabled, the SSH server needs to be configured as well. For OpenSSH, you’ll need to ensure that KbdInteractiveAuthentication
and UsePAM
are both set to yes
in /etc/ssh/sshd_config
.
If you want to require an authentication token for all authentication attempts, /etc/pam.d/common-auth
is the place to be:
# Comments removed to keep this short
auth [success=1 default=ignore] pam_unix.so nullok_secure
auth requisite pam_deny.so
auth required pam_permit.so
# Notice this line
auth required pam_google_authenticator.so nullok
Note that if you add the pam_google_authenticator
to the common-auth
, you should add nullok
parameter to the module. This allows logins from the users that do not have the authenticator configured. Without nullok
you’re effectively unable to authenticate, because you cannot log in to configure the authenticator. You shouldn’t install the authenticator configuration into the firmware image during the build unless it is really required, for reasons that are more discussed in the “About .google_authenticator file” -section. The downside of nullok
is of course that it allows logins from the users that do not have an authenticator configured, so you should ensure it gets configured after login.
That’s the Yocto part done. Building the image should result in an image that has all the required tools in place. Let’s get the image booted up and configure the MFA authenticator, shall we?

Configuring the Authenticator
The first step of the configuration is to ensure you have an authentication code generator. Since we’re using the Google Authenticator PAM module, using a Google Authenticator phone app is an easy choice. Another free alternative is FreeOTP app, sponsored and published by Red Hat. Or you could use this Python script to generate the codes. Or get a hardware generator. There are plenty of options. However, you must be able to trust the generator because a secret key needs to be input into it.
Once you have decided on your authentication code generation method, boot up your device with the image containing the setup tool and PAM library. Then, all we need to do is run google-authenticator
, and input the displayed secret key to the token generator (the configurator attempts to show a QR code as well, but that fails if you don’t have a display set up). There are a few configuration options, like disallowing code reuse, allowing time-skew, and rate-limiting. After finishing the configuration, ~/.google_authenticator
file gets created.
Below is an example configuration process output where I initialize MFA with time-based one-time passwords (TOTP), allow all the extra options, and finally I print out the generated configuration file. The secret key that needs to be input to the authenticator is Q4G4WZMCHHNTAS7Y7C4FG24T2Q in this example:
qemux86-64:~$ google-authenticator
Do you want authentication tokens to be time-based (y/n) y
Warning: pasting the following URL into your browser exposes the OTP secret to Google:
https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/serviceuser@qemux86-64%3Fsecret%3DQ4G4WZMCHHNTAS7Y7C4FG24T2Q%26issuer%3Dqemux86-64
Failed to use libqrencode to show QR code visually for scanning.
Consider typing the OTP secret into your app manually.
Your new secret key is: Q4G4WZMCHHNTAS7Y7C4FG24T2Q
Enter code from app (-1 to skip): 322615 Code confirmed
Your emergency scratch codes are:
35213877
85701662
19445473
37036890
83349037
Do you want me to update your "/home/serviceuser/.google_authenticator" file? (y/n) y
Do you want to disallow multiple uses of the same authentication
token? This restricts you to one login about every 30s, but it increases
your chances to notice or even prevent man-in-the-middle attacks (y/n) y
By default, a new token is generated every 30 seconds by the mobile app.
In order to compensate for possible time-skew between the client and the server,
we allow an extra token before and after the current time. This allows for a
time skew of up to 30 seconds between authentication server and client. If you
experience problems with poor time synchronization, you can increase the window
from its default size of 3 permitted codes (one previous code, the current
code, the next code) to 17 permitted codes (the 8 previous codes, the current
code, and the 8 next codes). This will permit for a time skew of up to 4 minutes
between client and server.
Do you want to do so? (y/n) y
If the computer that you are logging into isn't hardened against brute-force
login attempts, you can enable rate-limiting for the authentication module.
By default, this limits attackers to no more than 3 login attempts every 30s.
Do you want to enable rate-limiting? (y/n) y
qemux86-64:~$ cat .google_authenticator
Q4G4WZMCHHNTAS7Y7C4FG24T2Q
" RATE_LIMIT 3 30
" WINDOW_SIZE 17
" DISALLOW_REUSE
" TOTP_AUTH
35213877
85701662
19445473
37036890
83349037
In general, if you have a device that has a synchronized time available, you’ll most likely want to use TOTP where the authentication token gets rotated every 30 seconds. HMAC-based one-time passwords (HOTP) may have some desynchronization issues if the counters on the client and server aren’t updated at the same time, but in case there’s no reliable system time available, it’s a good alternative. Using HOTP requires a bit different PAM library configuration, so check out the GitHub repo for more information on that.
About .google_authenticator File
The .google_authenticator
file created in the home directory contains secret information. Therefore, you need to figure out a way to keep the contents secret. The permissions of the file have to be 600
, and the owner & group are the user who uses the authenticator key to login (because they need to be able to read it). This already should provide some level of security for the file. The home directory is the default location where the configuration gets read from during login, but it can be configured to be something else as well. Check out the GitHub repo README for more information on this.
In addition, encrypting or sealing the secret key into a TPM could help to secure it, but note that the file needs to be available and decrypted before the pam_google_authenticator
PAM library attempts to read it. This in practice means that the key needs to be decrypted in the file system before a successful login, making the extra efforts to secure it slightly less useful. Still, these may help to keep the key secure when the data is at rest (i.e. the device is powered off).
Note that since the secret key needs to be in a readable format in the system, it is a bad idea to rely solely on authentication codes. If someone has access to the device or its file system while the device is running, it might actually be even easier to read the plain text secret key than the encrypted password.
The strength of the possession factor in MFA is primarily in securing the remote logins. When the user is logging in to a secured system, they are not actually inputting the secret they know (unlike with passwords or other knowledge factor secrets). They input a token derived from the secret that the system and the user’s token generator know, and the secret itself remains secret. This makes it important for the generator to be trustworthy.
If you install .google_authenticator
file as a part of the Yocto build, this in practice means that all the devices can be unlocked using the same MFA tokens. While this in some cases may be desirable and ease the maintenance headache, it is also a security risk. If any of the devices happen to have their secret key leaked, it means that all the devices become compromised. So, if at all possible, the secret key should be generated on the device during an initial configuration process to ensure uniqueness.
Testing
Testing the feature is a bit anti-climatic, because all you need to do after the configuration is log out and attempt a log in again, entering the password and the authentication token when so requested:
qemux86-64 login: serviceuser
Password: password
Verification code: 123456
If you fail to log in, double-check the PAM configuration and ensure that the device you’re logging into has the correct system time. The authentication also fails if .google_authenticator
file does not have the correct mode or owner, so if you’re doing some testing where you install it manually, double-check that. To debug the possible issues, you may want to enable the MFA only for SSH logins, and then inspect the failure logs using a local serial connection that does not require the extra authentication.
Conclusion
As you can see, setting up MFA with PAM is quite straightforward. At least I was expecting it to require a lot more work. Once again we have the all too familiar problem of storing secrets in an embedded device, but the added authentication security is quite worth it, especially for the devices where remote access is possible.
Of course, you need to carefully plan the authenticator enrollment to ensure that it is manageable (both for you and the end users), works properly, and doesn’t leave the users locked out of the system. Also, in case the secret key leaks, you should have a plan on how to regenerate the key and update the authenticators. So as usual, the hard part isn’t enabling the feature, it is maintaining the feature.
That’s all for now, thanks for reading and comments are always welcome!
The other Yocto Hardening posts can be found here!
Recommended Reading
This time there wasn’t that much extra documentation that I would have consumed besides the google-authenticator-libpam README that was linked before. But, if you want to read some more about the basics, here are some (=two) links:
- Multi-Factor Authentication: Wikipedia
- Good and thorough explanation of the MFA
- Arch Wiki: Pam
- More information about the PAM. It also lists quite a few modules if you’re interested in those.