pam-oauth

module
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 16, 2024 License: MIT

README

PAM OAuth Module

Release Status Security Status

A Pluggable Authentication Module (PAM) and optional Name Service Switch (NSS) for OAuth, with optional support for OpenID Connect (OIDC).

[!WARNING]
This project is under active development and is not yet ready for production use.

Documentation

Support

  • Linux (x86-64/amd64) with glibc >= 2.31
  • Linux (arm64/aarch64) with glibc >= 2.31
Setup
  1. Download the latest release from the releases page
  2. Extract/install the client on the client machine and the server on the server machine, for example:
VERSION="X.Y.Z" # Get the latest semantic version (Without the "v" prefix!) from the releases page

# Debian/Ubuntu (x86-64/amd64)
wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-client_${VERSION}_amd64.deb
sudo dpkg -i pam-oauth-client_${VERSION}_amd64.deb

wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-server_${VERSION}_amd64.deb
sudo dpkg -i pam-oauth-server_${VERSION}_amd64.deb

# Debian/Ubuntu (arm64/aarch64)
wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-client_${VERSION}_arm64.deb
sudo dpkg -i pam-oauth-client_${VERSION}_arm64.deb

wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-server_${VERSION}_arm64.deb
sudo dpkg -i pam-oauth-server_${VERSION}_arm64.deb

# Red Hat/CentOS (x86-64/amd64)
wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-client-${VERSION}-1.x86_64.rpm
sudo rpm -i pam-oauth-client-${VERSION}-1.x86_64.rpm

wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-server-${VERSION}-1.x86_64.rpm
sudo rpm -i pam-oauth-server-${VERSION}-1.x86_64.rpm

# Red Hat/CentOS (arm64/aarch64)
wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-client-${VERSION}-1.aarch64.rpm
sudo rpm -i pam-oauth-client-${VERSION}-1.aarch64.rpm

wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-server-${VERSION}-1.aarch64.rpm
sudo rpm -i pam-oauth-server-${VERSION}-1.aarch64.rpm

# Arch Linux (x86-64/amd64)
wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-client-${VERSION}-1-x86_64.pkg.tar.zst
sudo pacman -U pam-oauth-client-${VERSION}-1-x86_64.pkg.tar.zst

wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-server-${VERSION}-1-x86_64.pkg.tar.zst
sudo pacman -U pam-oauth-server-${VERSION}-1-x86_64.pkg.tar.zst

# Arch Linux (arm64/aarch64)
wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-client-${VERSION}-1-aarch64.pkg.tar.zst
sudo pacman -U pam-oauth-client-${VERSION}-1-aarch64.pkg.tar.zst

wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-server-${VERSION}-1-aarch64.pkg.tar.zst
sudo pacman -U pam-oauth-server-${VERSION}-1-aarch64.pkg.tar.zst

# Alpine Linux (x86-64/amd64)
wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-client_${VERSION}_x86_64.apk
sudo apk add pam-oauth-client_${VERSION}_x86_64.apk

wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-server_${VERSION}_x86_64.apk
sudo apk add pam-oauth-server_${VERSION}_x86_64.apk

# Alpine Linux (arm64/aarch64)
wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-client_${VERSION}_aarch64.apk
sudo apk add pam-oauth-client_${VERSION}_aarch64.apk

wget -q https://github.com/Wakeful-Cloud/pam-oauth/releases/download/v${VERSION}/pam-oauth-server_${VERSION}_aarch64.apk
sudo apk add pam-oauth-server_${VERSION}_aarch64.apk
  1. Initialize the server:
# The localhost is so the client can be installed on the same machine as the server (Feel free to omit this if the client will never be installed on the same machine as the server)
sudo pam-oauth-server initialize --server-common-name=<server hostname> --server-dns-san=<alternate server hostname> --server-dns-san=localhost --server-ip-san=127.0.0.1 --server-ip-san=::1 --server-ip-san=<alternate server IP>
  1. Update the server configuration (e.g.: OAuth provider's details, listening address) in /etc/pam-oauth/server.toml

  2. Add a client:

sudo pam-oauth-server client add --client-common-name=<client hostname> --client-dns-san=<alternate client hostname> --client-ip-san=<client IP> --client-cert=<path to save client certificate to> --client-key=<path to save client key to>

Note: if the server is already running, you'll need to restart it for the changes to take effect.

  1. Initialize the client:
sudo pam-oauth-client initialize
  1. Update the client configuration (e.g.: server's address) in /etc/pam-oauth/client.toml

  2. Start the server:

# If using systemd
sudo systemctl start pam-oauth-server

# Or manually
sudo pam-oauth-server serve
  1. Update the PAM configuration (e.g.: /etc/pam.d/sshd):
+ auth sufficient pam_oauth.so /usr/bin/pam-oauth-client --config /etc/pam-oauth/client.toml run

# All other auth rules
@include common-auth

Note: the sufficient keyword means that if this module succeeds, the rest of the auth stack (i.e.: password or key-based authentication) will be skipped.

  1. Update the NSS configuration (e.g.: /etc/nsswitch.conf):
- passwd: files systemd
- group: files systemd
+ passwd: files systemd oauth
+ group: files systemd oauth
  1. Update the SSH server configuration (e.g.: /etc/ssh/sshd_config):
- ChallengeResponseAuthentication no
- KbdInteractiveAuthentication no
- UsePAM no
+ ChallengeResponseAuthentication yes
+ KbdInteractiveAuthentication yes
+ UsePAM yes
  1. Restart the SSH server:
sudo systemctl restart sshd
Client Configuration
Prompt Message

The prompt message template is a Go text template which is used to generate the message to prompt the user to open the authentication URL. The following variables are available:

  • .Username: the username of the user attempting to authenticate
  • .Url: the authentication URL
Create User Command

The create user command is a shell command which is used to create a user. The environment variables that are passed to the command are determined by the callback expression along with the following:

  • PAM_OAUTH_USERNAME: the username of the user attempting to authenticate
Server Configuration
Callback Expression

The callback expression is an Expr (Domain-Specific Language/DSL) expression which must:

  • Verify that the username of the user attempting to authenticate matches the username of the user who authenticated with the OAuth provider
  • Verify that the user is authorized to use PAM OAuth
  • Return any and all variables that are required by the create user command
Variables

The following variables are passed to the callback expression:

  • username: string: the username of the user attempting to authenticate
  • accessToken: string: the raw access token
  • refreshToken: string: the raw refresh token
  • oauthToken: struct: the OAuth token returned by the OAuth provider
    • expiry: time.Time: the expiry time of the token
    • type: string: the type of the token
  • idToken: struct | nil: the ID token returned by the OIDC provider (Only if the oidc_url is set in the OAuth client configuration)
    • accessTokenHash: string: the access token hash
    • audience: []string: the audience of the token
    • claims: map[string]any: the claims of the token
    • expiry: time.Time: the expiry time of the token
    • issuedAt: time.Time: the time the token was issued
    • issuer: string: the issuer of the token
    • nonce: string: the nonce of the token
    • raw: string: the raw token
    • subject: string: the subject of the token
  • clientCert: struct: the PAM OAuth client certificate
    • subject: string: the subject of the certificate
    • issuer: string: the issuer of the certificate
    • dnsSans: []string: the DNS subject alternative names of the certificate
    • ipSans: []net.IP: the IP subject alternative names of the certificate
    • serialNumber: string: the serial number of the certificate
    • signature: string: the hex-encoded signature of the certificate
    • signatureAlgorithm: string: the signature algorithm of the certificate
    • validFrom: time.Time: the time the certificate is valid from
    • validTo: time.Time: the time the certificate is valid to
    • keyUsage: []string: the key usages of the certificate
    • extKeyUsage: []string: the extended key usages of the certificate
Return Values

The following return values are expected to be returned by the callback expression:

  • ok: bool: whether or not to allow the user to authenticate
  • message: string: the message to show to the user if rejected
  • env: map[string]string: the environment variables to pass to the create user command running on the client (Note that PAM_OAUTH_USERNAME is always passed to the create user command)
Functions

The following functions are available in the callback expression:

  • Email address utilities
    • parseEmail: parses an email address into its name and domain
      • Parameters:
        • string: the email address (e.g.: <Name> username@example.com)
      • Returns:
        • struct
          • ok: bool: whether the email address is valid
          • name: string | nil: the name part of the email address (e.g.: Name), if ok is true and present
          • local: string: the local part of the email address (e.g.: username), if ok is true
          • domain: string: the domain part of the email address (e.g.: example.com), if ok is true
  • JSON Web Token (JWT) utilities
    • parseJwt: decodes a JWT and returns the claims
      • Parameters:
        • string: the JWT
        • string: the secret key
      • Returns:
        • struct
          • ok: bool: whether the token is valid (e.g.: not expired, signed with the provided secret, etc.)
          • header: map[string]any | nil: the header, if ok is true
          • claims: map[string]any | nil: the claims, if ok is true
  • Regular Expression (RegEx) utilities
    • execRegex: executes an RE2 regular expression and returns the first match's capturing groups
      • Parameters:
        • string: the regular expression pattern
        • string: the input string
      • Returns:
        • []string: the capturing groups (Including the full match as the first element)
    • execRegexAll: executes an RE2 regular expression and returns all matches' capturing groups
      • Parameters:
        • string: the regular expression pattern
        • string: the input string
      • Returns:
        • [][]string: the capturing groups (Including the full match as the first element of each match)
    • replaceRegex: replaces first occurrences of a regular expression pattern with a replacement string
      • Parameters:
        • string: the regular expression pattern
        • string: the input string
        • replacement: string: the replacement string
      • Returns:
        • string: the input string with all occurrences of the pattern replaced with the replacement string
    • replaceRegexAll: replaces all occurrences of a regular expression pattern with a replacement string
      • Parameters:
        • string: the regular expression pattern
        • string: the input string
        • string: the replacement string
      • Returns:
        • string: the input string with all occurrences of the pattern replaced with the replacement string
  • Miscellaneous utilities
    • log: logs a message to the server log
      • Parameters:
        • string: the log level (One of DEBUG, INFO, WARN, or ERROR)
        • string: the message to log
Examples
1. Simple verification
// Get the email from the ID token
let email = idToken?.claims?.email;

// Assertions
let emailOk = email != nil;
let usernameOk = email == username;

// Return
!emailOk
  ? {
      ok: false,
      message: "Your email address is invalid",
      env: {},
    }
  : !usernameOk
  ? {
      ok: false,
      message: "Your username does not match your email address",
      env: {},
    }
  : {
      ok: true,
      message: "",
      env: {
        "COMMENT": email
      },
    }

Note: this expression will still allow anyone who succesfully authenticates with the OAuth provider to authenticate with PAM OAuth. Furthermore, users must connect using their full email address when using SSH (e.g.: ssh username@mail.example.com@ssh.example.com) .

2. Domain-restricted verification
// Settings
let allowedDomains = ["mail.example.com"];
let allowedUsers = ["user1"];

// Get the email from the ID token
let email = idToken?.claims?.email;

// Parse the email
let parsedEmail = email != nil ? parseEmail(email) : nil;

// Assertions
let emailOk = parsedEmail != nil && parsedEmail.ok;
let emailDomainOk = parsedEmail?.domain in allowedDomains;
let emailLocalOk = parsedEmail?.local == username;
let usernameOk = username in allowedUsers;

// Return
!emailOk
  ? {
      ok: false,
      message: "Your email address is invalid",
      env: {},
    }
  : !emailDomainOk
  ? {
      ok: false,
      message:
        "Your email domain is not allowed (Expected one of: " +
        join(allowedDomains, ", ") +
        ", got: " +
        parsedEmail.domain +
        ")",
      env: {},
    }
  : !emailLocalOk
  ? {
      ok: false,
      message:
        "Your username does not match your email address (Expected: " +
        parsedEmail.local +
        ", got: " +
        username +
        ")",
      env: {},
    }
  : !usernameOk
  ? {
      ok: false,
      message: "You are not authorized to use PAM OAuth",
      env: {},
    }
  : {
      ok: true,
      message: "",
      env: {
        "COMMENT": email
      },
    }

Note: this expression will only allow users with an email address from the mail.example.com subdomain to authenticate with PAM OAuth. Furthermore, users must connect using only the local part of their email address when using SSH (e.g.: ssh username@mail.example.com). If you allow multiple domains (instead of a single domain, as with the above), be careful to ensure that the username is unique across all domains (e.g.: suffix the username with some form of the domain name).

Troublshooting
Server fails to start
Symptoms
  • The server fails to start
Resolution
  1. Verify that the server is not already running:
sudo systemctl status pam-oauth-server
  1. Check the server logs for errors:
sudo journalctl -u pam-oauth-server
  1. Check the server configuration for misconfigurations (See the setup instructions):
sudo cat /etc/pam-oauth/server.toml
  1. Manually start the server:
sudo pam-oauth-server serve
SSH login fails
Symptoms
  • When connecting to a server configured with PAM OAuth, SSH never prompts the user to authenticate with PAM OAuth (i.e.: it reverts to other authentication methods, such as password or key-based authentication)
Resolution
  1. Restart the SSH server:
sudo systemctl restart sshd
  1. Check the SSH server logs for errors:
sudo journalctl -u ssh
# Or
sudo journalctl -u sshd
  1. Check the PAM configuration for misconfigurations (See the setup instructions):
sudo cat /etc/pam.d/sshd
  1. Check the NSS configuration for misconfigurations (See the setup instructions):
sudo cat /etc/nsswitch.conf
  1. Check the SSH server configuration for misconfigurations (See the setup instructions):
sudo cat /etc/ssh/sshd_config
  1. Check the PAM OAuth client configuration for misconfigurations (See the setup instructions):
sudo cat /etc/pam-oauth/client.toml
  1. Manually run the PAM OAuth client:
sudo PAM_RHOST="localhost" PAM_RUSER="username" PAM_SERVICE="sshd" PAM_TTY="tty0" PAM_USER="username" PAM_TYPE="pam_sm_authenticate" pam-oauth-client run

Note: replace username with the username of the user attempting to authenticate.

Development Setup
  1. Install the tools:
  1. Clone the repository:
git clone --recursive https://github.com/wakeful-cloud/pam-oauth.git
  1. Install dependencies:
go mod download
Audit

You can run all security audits with:

task audit
Build

You can build everything with:

task build
Package

You can package everything with:

task package
Testing
  1. Build everything using the instructions above
  2. Initialize the server
./dist/bin/pam-oauth-server --config ./dev/server.toml initialize --server-common-name localhost  --server-ip-san 127.0.0.1 --server-ip-san ::1 --server-ip-san 172.17.0.1
  1. Update the server configuration (e.g.: OAuth provider's details, listening address)

  2. Add the client:

./dist/bin/pam-oauth-server --config ./dev/server.toml client add --client-common-name test --client-cert ./dev/internal-client.crt --client-key ./dev/internal-client.key
  1. Initialize the client:
./dist/bin/pam-oauth-client --config ./dev/client.toml initialize
  1. Update the client configuration (e.g.: server's address)

  2. Start the server:

go run ./cmd/server --config ./dev/server.toml serve
  1. Start the persistent container:
docker run -it -d -v $(pwd):/go/src/github.com/wakeful-cloud/pam-oauth --name pam-oauth ubuntu:latest
  1. Attach to the container:
docker exec -it pam-oauth /bin/bash
  1. Setup the container:
# Install dependencies
apt update
apt install -y libpam0g libpam0g-dev nano openssh-server openssl

# Setup SSH
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.old
sed -i -E -e 's/#?ChallengeResponseAuthentication no/ChallengeResponseAuthentication yes/' -e '#?KbdInteractiveAuthentication no/KbdInteractiveAuthentication yes/' -e 's/#?UsePAM no/UsePAM yes/' -e 's/#?PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
ssh-keygen -a 100 -t ed25519 -f /root/.ssh/id_ed25519 -N ""
service ssh start && service ssh stop # Fix directory creation bug

# Setup PAM
cp /etc/pam.d/sshd /etc/pam.d/sshd.old
sed -i -E -e '1iauth sufficient /go/src/github.com/wakeful-cloud/pam-oauth/dist/bin/pam_oauth.so /go/src/github.com/wakeful-cloud/pam-oauth/dist/bin/pam-oauth-client --config /go/src/github.com/wakeful-cloud/pam-oauth/dev/client.toml run' /etc/pam.d/sshd

# Setup NSS
cp /etc/nsswitch.conf /etc/nsswitch.conf.old
sed -i -E -e 's/passwd: (.*)/passwd: \1 oauth/' -e 's/group: (.*)/group: \1 oauth/' /etc/nsswitch.conf

# Link the shared libraries
ln -s /go/src/github.com/wakeful-cloud/pam-oauth/dist/bin/libnss_oauth.so /lib/x86_64-linux-gnu/libnss_oauth.so
ln -s /go/src/github.com/wakeful-cloud/pam-oauth/dist/bin/libnss_oauth.so /lib/x86_64-linux-gnu/libnss_oauth.so.2

# Configure the login shell permissions
chown root:root /go/src/github.com/wakeful-cloud/pam-oauth/dist/bin/pam-oauth-login
chmod 6755 /go/src/github.com/wakeful-cloud/pam-oauth/dist/bin/pam-oauth-login
  1. Start the SSH server:
/usr/sbin/sshd -D -d
  1. In a new terminal, attatch to the container again and attempt to authenticate over SSH:
ssh username@localhost
Structure
  • dist/: build artifacts
    • bin/: compiled binaries
      • pam-oauth-client: client binary
      • pam-oauth-login: login shell binary
      • pam-oauth-server: server binary
    • lib/: shared libraries
      • pam_oauth.so: PAM module shared library
      • libnss_oauth.so: NSS module shared library
    • man/: man pages
    • pkg/: package archives
  • cmd/: command line interfaces
    • client/: client command
    • login/: login shell command
    • server/: server command
  • internal/*: internal packages
  • lib/*: C shared libraries
    • nss.c: NSS stub resolver
    • pam.c: PAM module wrapper
PAM Wrapper Protocol

The PAM module wrapper and client executable communicate using NDJSON over standard output.

Standard Output Messages
prompt
  • type: string: "prompt"
  • style: int: the prompt style (1: echo off, 2: echo on, 3: error, 4: text info)
  • message: string: the message to prompt the user
putenv
  • type: string: "putenv"
  • name: string: the name of the environment variable
  • value: string: the value of the environment variable

Directories

Path Synopsis
cmd
internal
api

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL