mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-02 14:59:53 +08:00
* Move configuration from lib/service to lib/service/servicecfg The new servicecfg package will hold only configuration for services. This will allow other packages (like tctl and tsh) to depend on servicecfg without pulling in all of lib/service (which has a number of platform-specific details). This is the first step towards being able to build tctl for Windows. * Move PAM and BPF config into servicecfg This breaks a compile-time dependency on BPF/PAM for tctl.
509 lines
17 KiB
Go
509 lines
17 KiB
Go
//go:build pam && cgo
|
|
// +build pam,cgo
|
|
|
|
/*
|
|
Copyright 2018 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package pam
|
|
|
|
// #cgo LDFLAGS: -ldl
|
|
// #include <stdio.h>
|
|
// #include <stdlib.h>
|
|
// #include <string.h>
|
|
// #include <unistd.h>
|
|
// #include <dlfcn.h>
|
|
// #include <security/pam_appl.h>
|
|
// extern char *library_name();
|
|
// extern char* readCallback(int, int);
|
|
// extern void writeCallback(int n, int s, char* c);
|
|
// extern struct pam_conv *make_pam_conv(int);
|
|
// extern int _pam_start(void *, const char *, const char *, const struct pam_conv *, pam_handle_t **);
|
|
// extern int _pam_putenv(void *, pam_handle_t *, const char *);
|
|
// extern int _pam_end(void *, pam_handle_t *, int);
|
|
// extern int _pam_authenticate(void *, pam_handle_t *, int);
|
|
// extern int _pam_acct_mgmt(void *, pam_handle_t *, int);
|
|
// extern int _pam_open_session(void *, pam_handle_t *, int);
|
|
// extern int _pam_close_session(void *, pam_handle_t *, int);
|
|
// extern char **_pam_getenvlist(void *handle, pam_handle_t *pamh);
|
|
// extern const char *_pam_strerror(void *, pam_handle_t *, int);
|
|
// extern int _pam_envlist_len(char **);
|
|
// extern char * _pam_getenv(char **, int);
|
|
import "C"
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/lib/service/servicecfg"
|
|
)
|
|
|
|
func init() {
|
|
// Lock all PAM commands to the startup thread. From LockOSThread docs:
|
|
//
|
|
// All init functions are run on the startup thread. Calling LockOSThread from
|
|
// an init function will cause the main function to be invoked on that thread.
|
|
//
|
|
// This is needed for pam_loginuid.so. It writes "/proc/self/loginuid"
|
|
// which, on Linux, depends on being called from a specific thread. If
|
|
// it's not running on the right thread, pam_loginuid.so may fail with
|
|
// EPERM sporadically.
|
|
//
|
|
// > Why the startup thread specifically?
|
|
// The kernel does some validation based on the thread context. I could
|
|
// not find what the kernel uses specifically. Some relevant code:
|
|
// https://github.com/torvalds/linux/blob/9d99b1647fa56805c1cfef2d81ee7b9855359b62/kernel/audit.c#L2284-L2317
|
|
// Locking to the startup thread seems to make the kernel happy.
|
|
// If you figure out more, please update this comment.
|
|
//
|
|
// > Why not call LockOSThread from pam.Open?
|
|
// By the time pam.Open gets called, more goroutines could've been
|
|
// spawned. This means that the main goroutine (running pam.Open) could
|
|
// get re-scheduled to a different thread.
|
|
//
|
|
// > Why does pam.Open run on the main goroutine?
|
|
// This is an assumption. As of today, this is true because teleport
|
|
// re-executes itself and calls pam.Open synchronously. If we change this
|
|
// later, loginuid can become flaky again.
|
|
//
|
|
// > What does OpenSSH do?
|
|
// OpenSSH has a separate "authentication thread" which does all the PAM
|
|
// stuff:
|
|
// https://github.com/openssh/openssh-portable/blob/598c3a5e3885080ced0d7c40fde00f1d5cdbb32b/auth-pam.c#L470-L474
|
|
//
|
|
// Some historic context:
|
|
// https://github.com/gravitational/teleport/issues/2476
|
|
runtime.LockOSThread()
|
|
}
|
|
|
|
var log = logrus.WithFields(logrus.Fields{
|
|
trace.Component: teleport.ComponentPAM,
|
|
})
|
|
|
|
const (
|
|
// maxMessageSize is the maximum size of a message to accept from PAM. In
|
|
// theory it should be PAM_MAX_MSG_SIZE which is defined as the "max size (in
|
|
// chars) of each messages passed to the application through the conversation
|
|
// function call". [1] Unfortunately this does not appear respected by OpenSSH,
|
|
// which takes any size message and writes it out. [2]
|
|
//
|
|
// However, rather than accepting a message of unknown size from C code,
|
|
// increase the maximum size to about 1 MB. This will allow Teleport to
|
|
// print even long MOTD messages while not allowing C code to allocate
|
|
// unbound memory in Go.
|
|
//
|
|
// [1] http://pubs.opengroup.org/onlinepubs/008329799/apdxa.htm
|
|
// [2] https://github.com/openssh/openssh-portable/blob/V_8_0/auth-pam.c#L615-L654
|
|
maxMessageSize = 2000 * C.PAM_MAX_MSG_SIZE
|
|
|
|
// maxEnvironmentVariableSize is the maximum size of any environment
|
|
// variable. Even though pam_env.so sets this to a maximum of 1024, set
|
|
// it to be 10x that because not all PAM modules follow that convention. [1]
|
|
//
|
|
// [1] https://github.com/linux-pam/linux-pam/blob/master/modules/pam_env/pam_env.c#L55
|
|
maxEnvironmentVariableSize = 1024 * 10
|
|
)
|
|
|
|
// handler is used to register and find instances of *PAM at the package level
|
|
// to enable callbacks from C code.
|
|
type handler interface {
|
|
// writeStream will write to the output stream (stdout or stderr or
|
|
// equivalent).
|
|
writeStream(int, string) (int, error)
|
|
|
|
// readStream will read from the input stream (stdin or equivalent).
|
|
readStream(bool) (string, error)
|
|
}
|
|
|
|
var handlerMu sync.Mutex
|
|
var handlerCount int
|
|
var handlers map[int]handler = make(map[int]handler)
|
|
|
|
//export writeCallback
|
|
func writeCallback(index C.int, stream C.int, s *C.char) {
|
|
handle, err := lookupHandler(int(index))
|
|
if err != nil {
|
|
log.Errorf("Unable to write to output stream: %v", err)
|
|
return
|
|
}
|
|
|
|
// Convert C string to a Go string with a max size of maxMessageSize
|
|
// (about 1 MB).
|
|
str := C.GoStringN(s, C.int(C.strnlen(s, C.size_t(maxMessageSize))))
|
|
|
|
// Write to the stream (typically stdout or stderr or equivalent).
|
|
handle.writeStream(int(stream), str)
|
|
}
|
|
|
|
//export readCallback
|
|
func readCallback(index C.int, e C.int) *C.char {
|
|
handle, err := lookupHandler(int(index))
|
|
if err != nil {
|
|
log.Errorf("Unable to read from input stream: %v", err)
|
|
return nil
|
|
}
|
|
|
|
var echo bool
|
|
if e == 1 {
|
|
echo = true
|
|
}
|
|
|
|
// Read from the stream (typically stdin or equivalent).
|
|
s, err := handle.readStream(echo)
|
|
if err != nil {
|
|
log.Errorf("Unable to read from input stream: %v", err)
|
|
return nil
|
|
}
|
|
|
|
// Return one less than PAM_MAX_RESP_SIZE to prevent a Teleport user from
|
|
// sending more than a PAM module can handle and to allow space for \0.
|
|
//
|
|
// Note: The function C.CString allocates memory using malloc. The memory is
|
|
// not released in Go code because the caller of the callback function (PAM
|
|
// module) will release it. C.CString will null terminate s.
|
|
n := int(C.PAM_MAX_RESP_SIZE)
|
|
if len(s) > n-1 {
|
|
return C.CString(s[:n-1])
|
|
}
|
|
return C.CString(s)
|
|
}
|
|
|
|
// registerHandler will register a instance of *PAM with the package level
|
|
// handlers to support callbacks from C.
|
|
func registerHandler(p *PAM) int {
|
|
handlerMu.Lock()
|
|
defer handlerMu.Unlock()
|
|
|
|
// The make_pam_conv function allocates struct pam_conv on the heap. It will
|
|
// be released by Close function.
|
|
handlerCount = handlerCount + 1
|
|
p.conv = C.make_pam_conv(C.int(handlerCount))
|
|
handlers[handlerCount] = p
|
|
|
|
return handlerCount
|
|
}
|
|
|
|
// unregisterHandler will remove the PAM handle from the package level map
|
|
// once no more C callbacks can come back.
|
|
func unregisterHandler(handlerIndex int) {
|
|
handlerMu.Lock()
|
|
defer handlerMu.Unlock()
|
|
|
|
delete(handlers, handlerIndex)
|
|
}
|
|
|
|
// lookupHandler returns a particular handler from the package level map.
|
|
func lookupHandler(handlerIndex int) (handler, error) {
|
|
handlerMu.Lock()
|
|
defer handlerMu.Unlock()
|
|
|
|
handle, ok := handlers[handlerIndex]
|
|
if !ok {
|
|
return nil, trace.BadParameter("handler with index %v not registered", handlerIndex)
|
|
}
|
|
|
|
return handle, nil
|
|
}
|
|
|
|
var buildHasPAM bool = true
|
|
var systemHasPAM bool = false
|
|
|
|
// pamHandle is a opaque handle to the libpam object.
|
|
var pamHandle unsafe.Pointer
|
|
|
|
func init() {
|
|
// Obtain a handle to the PAM library at runtime. The package level variable
|
|
// SystemHasPAM is updated to true if a handle is obtained.
|
|
//
|
|
// Note: Since this handle is needed the entire time Teleport runs, dlclose()
|
|
// is never called. The OS will cleanup when the process exits.
|
|
pamHandle = C.dlopen(C.library_name(), C.RTLD_NOW)
|
|
if pamHandle != nil {
|
|
systemHasPAM = true
|
|
}
|
|
}
|
|
|
|
// PAM is used to create a PAM context and initiate PAM transactions to check
|
|
// the users account and open/close a session.
|
|
type PAM struct {
|
|
// pamh is a handle to the PAM transaction state.
|
|
pamh *C.pam_handle_t
|
|
|
|
// conv is the PAM conversation function for communication between
|
|
// Teleport and the PAM module.
|
|
conv *C.struct_pam_conv
|
|
|
|
// retval holds the value returned by the last PAM call.
|
|
retval C.int
|
|
|
|
// stdin is the input stream which the conversation function will use to
|
|
// obtain data from the user.
|
|
stdin io.Reader
|
|
|
|
// stdout is the output stream which the conversation function will use to
|
|
// show data to the user.
|
|
stdout io.Writer
|
|
|
|
// stderr is the output stream which the conversation function will use to
|
|
// report errors to the user.
|
|
stderr io.Writer
|
|
|
|
// service_name is the name of the PAM policy to use.
|
|
service_name *C.char
|
|
|
|
// login is the *nix login that that is being used.
|
|
login *C.char
|
|
|
|
// handlerIndex is the index to the package level handler map.
|
|
handlerIndex int
|
|
|
|
// once is used to ensure that any memory allocated by C only has free
|
|
// called on it once.
|
|
once sync.Once
|
|
}
|
|
|
|
// Open creates a PAM context and initiates a PAM transaction to check the
|
|
// account and then opens a session.
|
|
func Open(config *servicecfg.PAMConfig) (*PAM, error) {
|
|
if config == nil {
|
|
return nil, trace.BadParameter("PAM configuration is required.")
|
|
}
|
|
err := config.CheckDefaults()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
p := &PAM{
|
|
pamh: nil,
|
|
stdin: config.Stdin,
|
|
stdout: config.Stdout,
|
|
stderr: config.Stderr,
|
|
}
|
|
|
|
// Both config.ServiceName and config.Username convert between Go strings to
|
|
// C strings. Since the C strings are allocated on the heap in Go code, this
|
|
// memory must be released (and will be on the call to the Close method).
|
|
p.service_name = C.CString(config.ServiceName)
|
|
p.login = C.CString(config.Login)
|
|
|
|
// C code does not know that this PAM context exists. To ensure the
|
|
// conversation function can get messages to the right context, a handle
|
|
// registry at the package level is created (handlers). Each instance of the
|
|
// PAM context has it's own handle which is used to communicate between C
|
|
// and a instance of a PAM context.
|
|
p.handlerIndex = registerHandler(p)
|
|
|
|
// Create and initialize a PAM context. The pam_start function will
|
|
// allocate pamh if needed and the pam_end function will release any
|
|
// allocated memory.
|
|
p.retval = C._pam_start(pamHandle, p.service_name, p.login, p.conv, &p.pamh)
|
|
if p.retval != C.PAM_SUCCESS {
|
|
return nil, p.codeToError(p.retval)
|
|
}
|
|
|
|
for k, v := range config.Env {
|
|
// Set a regular OS env var on this process which should be available
|
|
// to child PAM processes.
|
|
os.Setenv(k, v)
|
|
|
|
// Also set it via PAM-specific pam_putenv, which is respected by
|
|
// pam_exec (and possibly others), where parent env vars are not.
|
|
kv := C.CString(fmt.Sprintf("%s=%s", k, v))
|
|
// pam_putenv makes a copy of kv, so we can free it right away.
|
|
defer C.free(unsafe.Pointer(kv))
|
|
retval := C._pam_putenv(pamHandle, p.pamh, kv)
|
|
if retval != C.PAM_SUCCESS {
|
|
return nil, p.codeToError(retval)
|
|
}
|
|
}
|
|
|
|
// Trigger the "account" PAM hooks for this login.
|
|
//
|
|
// Check that the *nix account is valid. Checking an account varies based off
|
|
// the PAM modules used in the account stack. Typically this consists of
|
|
// checking if the account is expired or has access restrictions.
|
|
//
|
|
// Note: This function does not perform any authentication!
|
|
retval := C._pam_acct_mgmt(pamHandle, p.pamh, 0)
|
|
if retval != C.PAM_SUCCESS {
|
|
return nil, p.codeToError(retval)
|
|
}
|
|
|
|
if config.UsePAMAuth {
|
|
// Trigger the "auth" PAM hooks for this login.
|
|
//
|
|
// These would perform any extra authentication steps configured in the PAM
|
|
// stack, like per-session 2FA.
|
|
retval = C._pam_authenticate(pamHandle, p.pamh, 0)
|
|
if retval != C.PAM_SUCCESS {
|
|
return nil, p.codeToError(retval)
|
|
}
|
|
}
|
|
|
|
// Trigger the "session" PAM hooks for this login.
|
|
//
|
|
// Open a user session. Opening a session varies based off the PAM modules
|
|
// used in the "session" stack. Opening a session typically consists of
|
|
// printing the MOTD, mounting a home directory, updating auth.log.
|
|
p.retval = C._pam_open_session(pamHandle, p.pamh, 0)
|
|
if p.retval != C.PAM_SUCCESS {
|
|
return nil, p.codeToError(p.retval)
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// Close will close the session, the PAM context, and release any allocated
|
|
// memory.
|
|
func (p *PAM) Close() error {
|
|
// Close the PAM session. Closing a session can entail anything from
|
|
// unmounting a home directory and updating auth.log.
|
|
p.retval = C._pam_close_session(pamHandle, p.pamh, 0)
|
|
if p.retval != C.PAM_SUCCESS {
|
|
return p.codeToError(p.retval)
|
|
}
|
|
|
|
// Unregister handler index at the package level.
|
|
unregisterHandler(p.handlerIndex)
|
|
|
|
// Free any allocated memory on close.
|
|
p.free()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Environment returns the PAM environment variables associated with a PAM
|
|
// handle. For example pam_env.so reads in certain environment variables
|
|
// which then have to be set when spawning the users shell.
|
|
//
|
|
// Note that pam_getenvlist is used to fetch the list of PAM environment
|
|
// variables and it is the responsibility of the caller to free that memory.
|
|
// From http://man7.org/linux/man-pages/man3/pam_getenvlist.3.html:
|
|
//
|
|
// It should be noted that this memory will never be free()'d by libpam.
|
|
// Once obtained by a call to pam_getenvlist, it is the responsibility
|
|
// of the calling application to free() this memory.
|
|
func (p *PAM) Environment() []string {
|
|
// Get list of additional environment variables requested from PAM.
|
|
pam_envlist := C._pam_getenvlist(pamHandle, p.pamh)
|
|
defer C.free(unsafe.Pointer(pam_envlist))
|
|
|
|
// Find out how many environment variables exist and size the output
|
|
// slice. This is pushed to C to avoid doing pointer arithmetic in Go.
|
|
n := int(C._pam_envlist_len(pam_envlist))
|
|
env := make([]string, 0, n)
|
|
|
|
// Loop over all environment variables and convert them to a Go string.
|
|
for i := 0; i < n; i++ {
|
|
pam_env := C._pam_getenv(pam_envlist, C.int(i))
|
|
defer C.free(unsafe.Pointer(pam_env))
|
|
|
|
pam_env_size := C.int(C.strnlen(pam_env, C.size_t(maxEnvironmentVariableSize)))
|
|
env = append(env, C.GoStringN(pam_env, pam_env_size))
|
|
}
|
|
|
|
return env
|
|
}
|
|
|
|
// free will end the PAM transaction (which itself will free memory) and
|
|
// then manually free any other memory allocated.
|
|
func (p *PAM) free() {
|
|
// Only free memory one time to prevent double free bugs.
|
|
p.once.Do(func() {
|
|
// Terminate the PAM transaction.
|
|
retval := C._pam_end(pamHandle, p.pamh, p.retval)
|
|
if retval != C.PAM_SUCCESS {
|
|
log.Warnf("Failed to end PAM transaction: %v.\n", p.codeToError(retval))
|
|
}
|
|
|
|
// Release the memory allocated for the conversation function.
|
|
C.free(unsafe.Pointer(p.conv))
|
|
|
|
// Release strings that were allocated when opening the PAM context.
|
|
C.free(unsafe.Pointer(p.service_name))
|
|
C.free(unsafe.Pointer(p.login))
|
|
})
|
|
}
|
|
|
|
// writeStream will write to the output stream (stdout or stderr or
|
|
// equivalent).
|
|
func (p *PAM) writeStream(stream int, s string) (int, error) {
|
|
writer := p.stdout
|
|
if stream == syscall.Stderr {
|
|
writer = p.stderr
|
|
}
|
|
|
|
// Replace \n with \r\n so the message correctly aligned.
|
|
r := strings.NewReplacer("\r\n", "\r\n", "\n", "\r\n")
|
|
n, err := writer.Write([]byte(r.Replace(s)))
|
|
if err != nil {
|
|
return n, err
|
|
}
|
|
|
|
return n, nil
|
|
}
|
|
|
|
// readStream will read from the input stream (stdin or equivalent).
|
|
// TODO(russjones): At some point in the future if this becomes an issue, we
|
|
// should consider supporting echo = false.
|
|
func (p *PAM) readStream(echo bool) (string, error) {
|
|
// Limit the reader in case stdin is from /dev/zero or other infinite
|
|
// source.
|
|
reader := bufio.NewReader(io.LimitReader(p.stdin, int64(C.PAM_MAX_RESP_SIZE)-1))
|
|
text, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
|
|
return text, nil
|
|
}
|
|
|
|
// codeToError returns a human readable string from the PAM error.
|
|
func (p *PAM) codeToError(returnValue C.int) error {
|
|
// If an error is being returned, free any memory that was allocated.
|
|
defer p.free()
|
|
|
|
// Error strings are not allocated on the heap, so memory does not need
|
|
// released.
|
|
err := C._pam_strerror(pamHandle, p.pamh, returnValue)
|
|
if err != nil {
|
|
return trace.BadParameter(C.GoString(err))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// BuildHasPAM returns true if the binary was build with support for PAM
|
|
// compiled in.
|
|
func BuildHasPAM() bool {
|
|
return buildHasPAM
|
|
}
|
|
|
|
// SystemHasPAM returns true if the PAM library exists on the system.
|
|
func SystemHasPAM() bool {
|
|
return systemHasPAM
|
|
}
|