基于Linux PAM的SSH认证企业级实践
在上一篇文章《基于Pam-Python模块Linux SSH双因子认证的实际应用》中我讲到了使用Linux PAM模块实现了对ssh登录进行的扩展认证模块。其中对PAM模块的认证机制做了详细的介绍并做了个简单的Demo实现,但考虑到工程化(项目规范程度以及对接企业内部统一认证平台等)以及安全性(如审计、python源码保护等)考虑,之前的Demo还不能满足到企业级使用需求。结合笔者最近在学习Go语言方面,于是想看看使用Golang能不能也造个轮子,在Github上搜了一些关于Golang在PAM上面,大多是通过CGO基于PAM的标准库(C语言)开发的。在简单学习了CGO基本使用后,就大体实现了这个Go语言版的PAM认证模块的雏形了。
基本代码框架如下所示:
├── Makefile // Go编译出动态链接库 ├── conv.go // 处理pam会话 ├── mypam.go // 认证相关业务实现 ├── pam.go // 基于security/pam_appl.h标准库实现的pam相关接口 └── pam_c.go // CGO封装的相关接口(如getUID()等)
在conv.go中,主要是封装实现了Conversation函数,以实现用户与ssh会话等交互消息:
package main
/*
#cgo LDFLAGS: -lpam
#include <stdlib.h>
#include <security/pam_appl.h>
int do_conv(pam_handle_t* hdlr, int count, const struct pam_message** msgs, struct pam_response** responses) {
int err;
struct pam_conv* conv;
err = pam_get_item(hdlr, PAM_CONV, (const void**)&conv);
if(err != PAM_SUCCESS) {
return err;
}
return conv->conv(count, msgs, responses, conv->appdata_ptr);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
// MessageStyle is a style of Message
type MessageStyle int
const (
// MessageEchoOff is for messages that shouldn't gave an echo.
MessageEchoOff = MessageStyle(C.PAM_PROMPT_ECHO_OFF)
// MessageEchoOn is for messages that should have an echo.
MessageEchoOn = MessageStyle(C.PAM_PROMPT_ECHO_ON)
// MessageErrorMsg is for messages that should be displayed as an error.
MessageErrorMsg = MessageStyle(C.PAM_ERROR_MSG)
// MessageTextInfo is for textual blurbs to be spat out.
MessageTextInfo = MessageStyle(C.PAM_TEXT_INFO)
)
// Message represents something to ask / show in a Conv.Conversation call.
type Message struct {
Style MessageStyle
Msg string
}
// Handle is a handle type to hang the PAM methods off of.
type Handle struct {
Ptr unsafe.Pointer
}
func (hdl Handle) ptr() *C.pam_handle_t {
return (*C.pam_handle_t)(hdl.Ptr)
}
// Conversation passes on the specified messages.
func (hdl Handle) Conversation(_msg Message) (string, error) {
msgStruct := (*C.struct_pam_message)(C.malloc(C.sizeof_struct_pam_message))
msgStruct.msg_style = C.int(_msg.Style)
msgStruct.msg = C.CString(_msg.Msg)
defer C.free(unsafe.Pointer(msgStruct.msg))
defer C.free(unsafe.Pointer(msgStruct))
respStruct := C.malloc(C.sizeof_struct_pam_response)
defer C.free(respStruct)
msg := (*C.struct_pam_message)(unsafe.Pointer(msgStruct))
resp := (*C.struct_pam_response)(respStruct)
code := C.do_conv(hdl.ptr(), C.int(1), &msg, &resp)
if code != C.PAM_SUCCESS {
return "", fmt.Errorf("Got non-success from the function: %d", code)
}
ret := C.GoString(resp.resp)
C.free(unsafe.Pointer(resp.resp))
return ret, nil
}
在pam.go中,主要是通过CGO导出了pam相关的标准库接口(关于pam的标准库相关文档说明,可以参考:openpam文档)。所以在mypam.go中,只需要根据企业具体的认证平台对接并实现mypamAuthenticate逻辑即可。
package main
/*
#cgo LDFLAGS: -lpam -fPIC
#include <security/pam_appl.h>
#include <stdlib.h>
char *get_user(pam_handle_t *pamh);
int get_uid(char *user);
*/
import "C"
import "unsafe"
//export pam_sm_authenticate
//https://fossies.org/dox/openpam-20190224/pam__sm__authenticate_8c.html
func pam_sm_authenticate(pamh *C.pam_handle_t, flags, argc C.int, argv **C.char) C.int {
cUsername := C.get_user(pamh)
if cUsername == nil {
return C.PAM_USER_UNKNOWN
}
defer C.free(unsafe.Pointer(cUsername))
uid := int(C.get_uid(cUsername))
if uid < 0 {
return C.PAM_USER_UNKNOWN
}
hdl := Handle{unsafe.Pointer(pamh)}
user, _ := hdl.Conversation(
Message{
Style: MessageEchoOn,
Msg: "Welcome to Server, Username: ",
},
)
pwd, _ := hdl.Conversation(
Message{
Style: MessageEchoOff,
Msg: "Input Your Password: ",
},
)
// 此处mypamAuthenticate函数,在业务层实现相关认证逻辑
ok, err := mypamAuthenticate(uid, C.GoString(cUsername), user, pwd)
if err != nil {
pamLog("authenticate err: %v", err)
return C.PAM_AUTH_ERR
}
if !ok {
pamLog("authenticate failed(user:%s, pwd:%s)", idmUser, idmPwd)
return C.PAM_AUTH_ERR
}
return C.PAM_SUCCESS
}
//export pam_sm_setcred
//https://fossies.org/dox/openpam-20190224/pam__sm__setcred_8c.html
func pam_sm_setcred(pamh *C.pam_handle_t, flags, argc C.int, argv **C.char) C.int {
return C.PAM_SUCCESS
}
//export pam_sm_acct_mgmt
//https://fossies.org/dox/openpam-20190224/pam__sm__acct__mgmt_8c.html
func pam_sm_acct_mgmt(pamh *C.pam_handle_t, flags, argc C.int, argv **C.char) C.int {
return C.PAM_SUCCESS
}
//export pam_sm_open_session
//https://fossies.org/dox/openpam-20190224/pam__sm__open__session_8c.html
func pam_sm_open_session(pamh *C.pam_handle_t, flags, argc C.int, argv **C.char) C.int {
return C.PAM_SUCCESS
}
//export pam_sm_close_session
//https://fossies.org/dox/openpam-20190224/pam__sm__close__session_8c.html
func pam_sm_close_session(pamh *C.pam_handle_t, flags, argc C.int, argv **C.char) C.int {
return C.PAM_SUCCESS
}
//export pam_sm_chauthtok
//https://fossies.org/dox/openpam-20190224/pam__get__authtok_8c.html
func pam_sm_chauthtok(pamh *C.pam_handle_t, flags, argc C.int, argv **C.char) C.int {
return C.PAM_SUCCESS
}
在pam_c.go中,通过CGO封装实现了很多glibc中的通用函数,这里不一一说明,详细定义如下:
package main
/*
#include <errno.h>
#include <pwd.h>
#include <security/pam_appl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#ifdef __APPLE__
#include <sys/ptrace.h>
#elif __linux__
#include <sys/prctl.h>
#endif
char *string_from_argv(int i, char **argv) {
return strdup(argv[i]);
}
// get_user pulls the username out of the pam handle.
char *get_user(pam_handle_t *pamh) {
if (!pamh)
return NULL;
int pam_err = 0;
const char *user;
if ((pam_err = pam_get_item(pamh, PAM_USER, (const void**)&user)) != PAM_SUCCESS)
return NULL;
return strdup(user);
}
// owner_uid returns the owner of a given file, if can be read.
int owner_uid(char *path) {
struct stat sb;
int ret = -1;
if ((ret = stat(path, &sb)) < 0) {
return -1;
}
return (int)sb.st_uid;
}
// get_uid returns the uid for the given char *username
int get_uid(char *user) {
if (!user)
return -1;
struct passwd pw, *result;
char buf[8192]; // 8k should be enough for anyone
int i = getpwnam_r(user, &pw, buf, sizeof(buf), &result);
if (!result || i != 0)
return -1;
return pw.pw_uid;
}
// get_username returns the username for the given uid.
char *get_username(int uid) {
if (uid < 0)
return NULL;
struct passwd pw, *result;
char buf[8192]; // 8k should be enough for anyone
int i = getpwuid_r(uid, &pw, buf, sizeof(buf), &result);
if (!result || i != 0)
return NULL;
return strdup(pw.pw_name);
}
// change_euid sets the euid to the given euid
int change_euid(int uid) {
return seteuid(uid);
}
int disable_ptrace() {
#ifdef __APPLE__
return ptrace(PT_DENY_ATTACH, 0, 0, 0);
#elif __linux__
return prctl(PR_SET_DUMPABLE, 0);
#endif
return 1;
}
*/
import "C"
import (
"fmt"
"os/user"
"strconv"
"unsafe"
)
// ownerUID returns the uid of the owner of a given file or directory.
func ownerUID(path string) int {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))
return int(C.owner_uid(cPath))
}
// getUID is used for testing.
func getUID() int {
u, err := user.Current()
if err != nil {
fmt.Printf("user.Current error: %v\n", err)
return -1
}
i, err := strconv.Atoi(u.Uid)
if err == nil {
return i
}
cUsername := C.CString(u.Uid)
defer C.free(unsafe.Pointer(cUsername))
return int(C.get_uid(cUsername))
}
// getUsername returns the username associated with the given uid.
func getUsername(uid int) string {
cUsername := C.get_username(C.int(uid))
if cUsername == nil {
return "<unknown>"
}
defer C.free(unsafe.Pointer(cUsername))
return C.GoString(cUsername)
}
// seteuid drops privs.
func seteuid(uid int) bool {
return C.change_euid(C.int(uid)) == C.int(0)
}
// likely redundant, but try and make sure we can't be traced.
func disablePtrace() bool {
return C.disable_ptrace() == C.int(0)
}
值得注意的是,在业务上层实现mypamAuthenticate函数时,笔者在使用Go标准库的net/http进行socket发送数据包时发现无法无法使用net/http库。调试了会貌似还是没解决问题,于是就用C语言版socket TCP发包函数并使用CGO封装了下使用,具体实现如下:
package main
/*
#include <errno.h>
#include <pwd.h>
#include <security/pam_appl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include<arpa/inet.h>
#include <sys/prctl.h>
#define RECVBUF 1024
// create a http request with tcp
// hostname support ip or domain
char *make_request(char *hostname, int port, char *message) {
int sockfd;
char reply[RECVBUF];
struct sockaddr_in server;
struct in_addr addr;
struct hostent *h;
memset(reply, '\0', sizeof(reply));
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
return NULL;
}
if(! inet_pton(AF_INET, hostname, &addr)){
if((h = gethostbyname(hostname)) == NULL) {
return NULL;
}
hostname = inet_ntoa(*((struct in_addr *)h->h_addr));
}
server.sin_addr.s_addr = inet_addr(hostname);
server.sin_family = AF_INET;
server.sin_port = htons(port);
if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0) {
return NULL;
}
// TODO: set send timeout
if (send(sockfd, message, strlen(message), 0) < 0) {
return NULL;
}
// TODO: set recv timeout
if (recv(sockfd, reply, sizeof(reply), 0) < 0) {
return NULL;
}
close(sockfd);
return strdup(reply);
}
*/
import "C"
import (
"unsafe"
)
func doNetSend(host string, port int, msg string) string {
cHostName := C.CString(host)
defer C.free(unsafe.Pointer(cHostName))
cMsg := C.CString(msg)
defer C.free(unsafe.Pointer(cMsg))
resp := C.make_request(cHostName, C.int(port), cMsg)
defer C.free(unsafe.Pointer(resp))
return C.GoString(resp)
}
至此,就基本使用Golang语言实现的PAM的相关认证的底层设计了。在笔者的工程化实践中,通过mypamAuthenticate函数在业务上层实现了2fa认证,本地认证(基于session cache,类似Kerberos实现,在离线状态下的认证)和远端认证(对接企业内部的统一认证平台),以及多种认证的组合形式,达到了较为灵活且定制化的ssh授权认证管理中心。通过授权认证管理中心可以让谁在什么指定的时间,指定的客户端ip源,指定的账户进行授权访问。以及对登录登出的log进行记录并审计,通过实现pam_sm_open_session和pam_sm_close_session函数,可以将用户关联到实现shell的session以记录所有执行的shell命令。
参考文档:
openpam接口文档:https://fossies.org/dox/openpam-20190224/
PAM 模块开发入门:http://www.rkeene.org/projects/info/wiki/222
PAM开发实现2fa:https://ben.akrin.com/?p=1068
PAM相关开源项目参考:https://developers.yubico.com/yubico-pam/
Uber开源的ssh-pam模块:https://github.com/uber/pam-ussh