基于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



这篇博文由 s0nnet 于2020年01月31日发表在 GNU/Linux, Go语言, Linux系统, 代码艺术, 安全工具, 网络安全 分类下, 通告目前不可用,你可以至底部留下评论。
如无特别说明,独木の白帆发表的文章均为原创,欢迎大家转载,转载请注明: 基于Linux PAM的SSH认证企业级实践 | 独木の白帆
关键字: , ,

基于Linux PAM的SSH认证企业级实践:等您坐沙发呢!

发表评论

快捷键:Ctrl+Enter