이건 정리용.


1. Root CA 생성

# CA private key 생성
openssl genrsa -out ca.key 2048

# CA request 생성
openssl req -new -key ca.key -out ca.csr -subj "/C=KR/O=TIM Lab/CN=My VPN CA"

# CA 인증서 생성
echo "basicConstraints = critical, CA:TRUE
subjectKeyIdentifier = hash
keyUsage = digitalSignature, keyCertSign, cRLSign" > ca.ext

openssl x509 -req -days 3650 -extfile ca.ext -set_serial 1 -signkey ca.key -in ca.csr -out ca.crt

# 필요없는 설정파일과 csr 제거
rm ca.ext ca.csr


2. ta.key, dh2048.pem 생성 

openssl dhparam -out dh2048.pem 2048
openvpn --genkey --secret ta.key


3. 서버용 인증서 생성

# private key 생성
openssl genrsa -out cert.key 2048

# csr 생성
openssl req -new -key cert.key -out cert.csr -subj "/C=KR/O=My Organization/CN=VPN Server"

# CA 인증서/키로 인증서 생성
echo "basicConstraints = critical, CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth" > cert.ext

openssl x509 -req -days 3650 -extfile cert.ext -CA ca.crt -CAcreateserial -CAkey  ca.key -in cert.csr -out cert.crt

# 필요없는 설정파일과 csr 제거
rm cert.ext cert.csr

# 서버 인증서, 키 등 이동 / 복사
cp ca.crt /etc/openvpn/ca.crt
cp cert.key /etc/openvpn/server.key
cp ta.key /etc/openvpn/ta.key
mv cert.crt /etc/openvpn/server.crt



4. 클라이언트용 인증서 생성

사실 서버 인증서 생성과 큰 차이 없다. X.509 의 설정파일 내용 정도?
# private key 생성
openssl genrsa -out cert.key 2048

CERT_NAME="Gildong Hong"

# csr 생성
openssl req -new -key cert.key -out cert.csr -subj "/C=KR/O=My Organization/CN=$CERT_NAME"

# CA 인증서/키로 인증서 생성
echo "basicConstraints = critical, CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = digitalSignature" > cert.ext

openssl x509 -req -days 3650 -extfile cert.ext -CA ca.crt -CAcreateserial -CAkey  ca.key -in cert.csr -out "clients/$CERT_NAME.crt"

# 필요없는 설정파일과 csr 제거
rm cert.ext cert.csr



5. ovpn 설정파일 만들기

exec 3> VPN.ovpn

echo "client
remote SERVER_HOST PORT

dev tun
proto udp
resolv-retry infinite
nobind
cipher AES-256-CBC
auth SHA256
key-direction 1
persist-key
persist-tun
remote-cert-tls server
verb 3

;redirect-gateway def1 bypass-dhcp
;auth-user-pass
" >&3

printf "\n\n<ca>\n" >&3
cat ca.crt >&3
printf "</ca>\n\n<tls-auth>\n" >&3
cat ta.key >&3
printf "</tls-auth>\n\n<cert>\n" >&3
cat "clients/$CERT_NAME.crt" >&3
printf "</cert>\n\n<key>\n" >&3
cat cert.key >&3
printf "</key>" >&3

exec 3>&-


OpenVPN 은 기본적으로 사용자별로 인증서를 생성해서 연결하도록 하고 있다.


그러다보니 설정파일에 ca, tls-auth, cert, key 4개의 파일이 추가로 따라다니게 되는데 관리하기 조금 번거로운 면이 있다.

물론 각각 ovpn 파일안에 임베딩해버릴 수 있지만...


아이디, 비밀번호로 로그인하는 방법이 있길래 진행해보았다.


먼저 OpenVPN 서버 설정에서 아래 두 문구를 추가한다.

client-cert-not-required
auth-user-pass-verify "/etc/openvpn/userauth/verify.sh" via-file

첫번째 줄은 클라이언트 인증서가 필요없게 하는거고, 두번째줄은 비밀번호를 체크할 스크립트를 지정한다.

저 파일은 내가 만들어줘야하며, 경로는 어디두던 크게 상관없음.


다만 user nobody / group nogroup 설정을 했을 경우 OpenVPN 데몬이 nobody / nogroup 권한으로 작동하기 때문에, 권한에 유의해주면 된다. 읽기 권한이나 실행 권한 등이 없으면 비밀번호 체크를 할 수 없다.

비밀번호 체크 과정에 root 권한이 필요하다면 권한을 낮추는 해당 설정을 제거해주자.


스크립트 경로 뒤의 via-file 은 OpenVPN 에서 사용자가 입력한 아이디와 비밀번호를 넘겨줄 방식을 정한다.

via-file 로 할 경우 임시 파일을 생성하면서

username
password

이렇게 두 줄로 된 파일을 생성한다.


(via-env 로 할 경우 환경변수에 넣어준다는데, 사용자명은 들어오는데 비밀번호는 어디있는지 모르겠더라...)



해당 스크립트에서 아이디와 비밀번호를 읽어서, 성공했다면 exit 0 을, 실패했다면 에러코드와 함께 프로그램이 종료되면 된다.



이제 비밀번호 체크를 위한 스크립트를 만들자.

방식은 다양하니 직접 만들면 되겠지만 내 스크립트를 참고 삼아 올려둠.


아래 URL 에 들어가보면 다른 스크립트들도 있다.


나는 user.pass 파일에

사용자명=SHA1비밀번호
사용자명=SHA1비밀번호

꼴로 저장을 해두고, 이를 검사하는 방식으로 작성하였다.


verify.sh 코드는 이렇게.

#!/bin/bash
passfile="/etc/openvpn/userauth/user.pass"
logfile="/var/log/openvpn/userauth.log"

username=`head -n 1 $1`
password=`head -n 2 $1 | tail -1`

hashed=`echo -n $password | sha1sum | awk '{print $1}'`
userpass=`cat $passfile | grep $username= | awk -F= '{print $2}'`

if [ "$userpass" = "$hashed" ]; then
        echo "`date +'%Y-%m-%d %H:%M:%S'` - auth success: $username" >> $logfile
#       echo "ok"
        exit 0
fi

echo "`date +'%Y-%m-%d %H:%M:%S'` - auth fail: $username" >> $logfile
#echo "not ok"
exit 1



그리고 client 설정 파일에는 cert 와 key 를 없애고, auth-user-pass 옵션을 추가해주면 된다.



----------

이 다음으로, Local 사용자 계정으로 로그인하는걸 시도해보았다.


보통 shadow 파일을 읽어서 비교를 많이 하던데, 나는 PAM Authentication 을 이용해보았다.

PAM 인증을 할때 root 이어야만 제대로 작동하는 것 같으니, 서버 설정에서 user nobody 옵션은 주석처리 해주자. (혹은 스티키 비트를 넣어주거나..)



PAM 인증을 하는 c 프로그램을 제작하였고, 로그를 남기기 위해 간단한 쉘 스크립트를 같이 이용하였음.

verify.sh

#!/bin/bash
pam_auth="/etc/openvpn/userauth/pam_auth"
logfile="/var/log/openvpn/userauth.log"

pam_result=`$pam_auth $1`
ret=$?

echo "`date +'%Y-%m-%d %H:%M:%S'` $pam_result" >> $logfile
exit $ret



pam_auth.c ( https://gist.github.com/iolate/a58b73a023b35d5f181814de2f4ffccd )

// gcc -o pam_auth pam_auth.c -lpam

#include <security/pam_appl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int custom_converation(int num_msg, const struct pam_message** msg, struct pam_response** resp, void* appdata_ptr) {
	// Provide password for the PAM conversation response that was passed into appdata_ptr
	struct pam_response* reply = (struct pam_response* )malloc(sizeof(struct pam_response));
	reply[0].resp = (char*)appdata_ptr;
	reply[0].resp_retcode = 0;

	*resp = reply;
	return PAM_SUCCESS;
}

int main(int argc, char *argv[]) {
	if (argc != 2) {
		fprintf(stderr, "Usage: %s [filepath]\n", argv[0]);
		exit(1);
	}
	
	FILE* fp;
	char* username = NULL;
	char* password = NULL;
	size_t len = 0;
	ssize_t read;
	
	fp = fopen(argv[1], "r");
	if (fp == NULL) {
		fprintf(stderr, "%s: Cannot open '%s'\n", argv[0], argv[1]);
		return 1;
	}
	
	read = getline(&username, &len, fp);
	if (read == -1) {
		fclose(fp);
		return 1;
	}
	username[strlen(username)-1] = '\0'; // remove LF
	
	read = getline(&password, &len, fp);
	if (read == -1) {
		fclose(fp);
		return 1;
	}
	password[strlen(password)-1] = '\0'; // remove LF
	
	fclose(fp);
	
	// PAM Authentication
	struct pam_conv conv = {custom_converation, password};
	pam_handle_t* pamh = NULL;

	int retval = pam_start("whoami", username, &conv, &pamh);

	if (retval == PAM_SUCCESS)
		retval = pam_authenticate(pamh, 0); // is user really user?

	//if (retval == PAM_SUCCESS)
	//	retval = pam_acct_mgmt(pamh, 0); // permitted access?

	if (retval == PAM_SUCCESS) {
		fprintf(stdout, "Authenticated - %s\n", username);
	} else {
		fprintf(stdout, "Not Authenticated - %s\n", username);
	}

	pam_end(pamh, 0);
	return retval;
}


pam_auth 를 컴파일하기 위해선 pam development 패키지가 필요하니 설치하고 컴파일 해주자.

Ubuntu 기준 아래의 방법으로 진행하면 됨.

sudo apt install libpam0g-dev
gcc -o pam_auth pam_auth.c -lpam



verify.sh 를 간단히 수정하면, 두가지 방식을 동시에 사용하는 것도 가능하다.


참고

https://forums.openvpn.net/viewtopic.php?t=24907

https://medium.com/@nqbao/openvpn-auth-user-pass-verify-example-8d99023f08f7


https://unix.stackexchange.com/a/153323

http://www.linux-pam.org/Linux-PAM-html/adg-example.html

+ Recent posts