Консольное приложение на php с упаковкой в RPM-пакет, настройка цифровой подписи.

Для корректной цифровой подписи необходимо сгенерировать gpg-ключ командой

gpg --gen-key

В консольном режиме у меня его сгенерировать не вышло(требует поделать что-нибудь на машине при генерации, а это знаете ли проблема при подключении по ssh к удаленной машине).
Однако я сгенерировал его на виртуалке, а после перекинул папку .gnupg на удаленную машину в профиль пользователя ( /home/user/ ). Далее командой

gpg --list-key

проверяем ключ (первый раз он попросит ввести пароль, который вы указали при генерации ключа).

Получаем в вывод что-то типа такого:

/home/user/.gnupg/pubring.gpg
--------------------------------
pub   4096R/6G797EDE 2017-12-22
uid                  Sergei Surname (tel: <+375000000000 reserved: <test@gmail.com>) <admin@sergdudko.tk>
sub   4096R/1A0D19A1 2017-12-22

Берем отсюда uid и пишем его команщду экспорта:

gpg --export -a "Sergei Surname (tel: <+375290000000 reserved: <test@sergdudko.tk>) <admin@sergdudko.tk>" > RPM-GPG-KEY

Импортируем ключ командой:

sudo rpm --import RPM-GPG-KEY

Этот же ключ необходимо предоставить вместе с будущим приложением (например на сайте) для подтверждения подписи.
Проверяем, что ключ импортирован (ищем в списке) командой:

rpm -qi gpg-pubkey | grep Summary

Для сборки пакета нам понадобится пакет rpm-build, установим его командой:

yum install rpm-build -yum

Далее в профиле пользователя (все в упор рекомендуют это делать не из под рута, я собирал и под рутом, но сейчас так не делаю. почему? а хз, просто не делаю, ничего страшного под рутом не случалось) создаем структуру каталогов:

rpmbuild/
+BUILD/
+BUILDROOT/
+RPMS/
+SOURCES/
+SPECS/
+SRPMS/

Подготовка закончена. Чтобы наше приложение выполняло что-нибудь полезное займемся исполняемыми файлами. Я решил запилить пакет для автоматического развертывания vpn-серверов. Т.к. в баше я сильно не ковырялся, а в системе уже стоит интерпретатор php и сам php я немного знаю (пысы: ну возьмите меня кто-нибудь джуном за адекватные бабки) на нем и напишу.
Для начала возьмем пакеты из репозитория epel: openvpn и easy-rsa. Первый собственно будет сервером, второй работает с сертификатами, без которых сервер не развернуть (это отнюдь не означает, что их нужно генерировать на одной и той же машине).
После этого попробовали настроить сервер привычным способом. Для настройки я буду использовать tap-устройство для поддержки полноценного Ethernet (отличия tun и tap думаю и сами нагуглите). Тем более у меня есть купленное приложение для андроида с поддержкой tap (OpenVPN Client собственно) и рут ему не нужен.
Первым делом в любом случае необходимо сгенерировать сертификаты сервера, его конфигурационный файл, ну и собственно запустить сервер. Не забудьте разрешить нужные порты в фаерволе например так

firewall-cmd --zone=public --add-port=1700-1955/tcp

После того как проделал создание сервера вручную, описал это в скрипте:

Показать

<?php
//php /etc/openvpn/scripts/build_server.php --args -add 1
//php /etc/openvpn/scripts/build_server.php --args -remove 1
include(__DIR__ . '/settings.php'); 
$current = 'php /etc/openvpn/scripts/build_server.php --args '.$argv[2].' '.$argv[3] . PHP_EOL;
if(!file_exists('/etc/openvpn/dudko-web-panel/')) { 
  if(!mkdir('/etc/openvpn/dudko-web-panel/', 0755, true)) {
    echo 'Не удалось создать каталог .../dudko-web-panel/!';
    exit;
  }
}
if(!is_dir('/etc/openvpn/dudko-web-panel/logs')){ mkdir('/etc/openvpn/dudko-web-panel/logs');}
$file = '/etc/openvpn/dudko-web-panel/logs/server'.$argv[3].date("Y-m-d").'.log';
if(file_exists($file)){ $current .= file_get_contents($file); }
 
if($argv[2]=='-add'){
  	$db_connect = new mysqli($db_ipaddr, $db_user, $db_pass, $db_name); 
    if ($db_connect->connect_errno) {
      	echo "Не удалось подключиться к MySQL:" . $db_connect->connect_error;
        $current .= date("Y-m-d H:i:s").'     '."Не удалось подключиться к MySQL:" . $db_connect->connect_error;
        $current .= PHP_EOL;
      	echo PHP_EOL;
        file_put_contents($file, $current);  
        exit;
    } 
    $db_connect->query("SET NAMES utf8"); 
    $result = $db_connect->query("SELECT * FROM `server_conf` WHERE `Id` = ".strval($argv[3])."");
    while($row = $result->fetch_assoc()){
      	$variable = $row;
    }
    $db_connect->close();
  	if(!isset($variable['Id'])){
      	echo "Сервер отсутствует в MySQL!";
    	$current .= date("Y-m-d H:i:s").'     '."Сервер отсутствует в MySQL!";
        $current .= PHP_EOL;
      	echo PHP_EOL;
        file_put_contents($file, $current);  
        exit;
    }
	CreateServer('server'.$variable['Id'], intval($variable['Id']), $current, $variable);
}
if($argv[2]=='-remove'){
	RemoveServer('server'.$argv[3], $current);
}
$current .= PHP_EOL;
file_put_contents($file, $current);
echo PHP_EOL;
exit;
 
function cmd_exec($operation, $cmd, &$stdout, &$stderr)
{
    $outfile = tempnam(".", "cmd");
    $errfile = tempnam(".", "cmd");
    $descriptorspec = array(
        0 => array("pipe", "r"),
        1 => array("file", $outfile, "w"),
        2 => array("file", $errfile, "w")
    );
    $proc = proc_open($cmd, $descriptorspec, $pipes);
 
    if (!is_resource($proc)) return 255;
	if($operation == 'ca'){
		for($i=0;$i<8;$i++){ //8 переходов на новую строку для CA
			fwrite($pipes[0], "\n");
			usleep(200);
		}
	}
	if($operation == 'server'){
		for($i=0;$i<10;$i++){ //10 переходов на новую строку для SERVER, потом два раза yes и переход
			fwrite($pipes[0], "\n");
			usleep(200);
		}
		sleep(3);
		fwrite($pipes[0], "y\n");
		usleep(500);
		fwrite($pipes[0], "y\n");
	}
    fclose($pipes[0]); 
 
    $exit = proc_close($proc);
	$stdout = file($outfile);
    $stderr = file($errfile);
 
    unlink($outfile);
    unlink($errfile);
    return $exit;
}
 
function CreateServer($servername, $servernum, &$log, $variable){
	/* предустановки */
	if(!file_exists('/etc/openvpn/dudko-web-panel/settings/')) { 
		if(!mkdir('/etc/openvpn/dudko-web-panel/settings/', 0755, true)) {
			echo 'Не удалось создать каталог .../settings/!';
          	$log .= date("Y-m-d H:i:s").'     '.'Не удалось создать каталог .../settings/!';
  			$log .= PHP_EOL;
			return;
		}
	}
	if(!file_exists('/etc/openvpn/dudko-web-panel/rsa-key/')) { 
		if(!mkdir('/etc/openvpn/dudko-web-panel/rsa-key/', 0755, true)) {
			echo 'Не удалось создать каталог .../rsa-key/!';
          	$log .= date("Y-m-d H:i:s").'     '.'Не удалось создать каталог .../rsa-key/!';
  			$log .= PHP_EOL;
			return;
		}
	}
	if(!file_exists('/etc/openvpn/dudko-web-panel/easy-rsa/')) { 
		if(!mkdir('/etc/openvpn/dudko-web-panel/easy-rsa/', 0755, true)) {
			echo 'Не удалось создать каталог .../easy-rsa/!';
          	$log .= date("Y-m-d H:i:s").'     '.'Не удалось создать каталог .../easy-rsa/!';
  			$log .= PHP_EOL;
			return;
		} else {
			exec('cp -rp /usr/share/easy-rsa/2.0/* /etc/openvpn/dudko-web-panel/easy-rsa/', $callback);
			if(count($callback) != 0) {
				echo 'Возникли неполадки при копировании, выполните команду "cp -rp /usr/share/easy-rsa/2.0/* /etc/openvpn/dudko-web-panel/easy-rsa/" в shell!';
              	$log .= date("Y-m-d H:i:s").'     '.'Возникли неполадки при копировании, выполните команду "cp -rp /usr/share/easy-rsa/2.0/* /etc/openvpn/dudko-web-panel/easy-rsa/" в shell!';
  				$log .= PHP_EOL;
			};
		}
	}
   if(!file_exists('/etc/openvpn/dudko-web-panel/logs/')) { 
		if(!mkdir('/etc/openvpn/dudko-web-panel/logs/', 0755, true)) {
			echo 'Не удалось создать каталог .../logs/!';
          	$log .= date("Y-m-d H:i:s").'     '.'Не удалось создать каталог .../logs/!';
  			$log .= PHP_EOL;
			return;
		}
	}
 
	/* проверяем зависимости */
	if(file_exists('/etc/openvpn/dudko-web-panel/settings/'.$servername.'/')) { 
		echo 'Сервер уже существует(settings)!';
      	$log .= date("Y-m-d H:i:s").'     '.'Сервер уже существует(settings)!';
  		$log .= PHP_EOL;
		return;
	}
	if(file_exists('/etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/')) { 
		echo 'Сервер уже существует(rsa-key)!';
      	$log .= date("Y-m-d H:i:s").'     '.'Сервер уже существует(rsa-key)!';
  		$log .= PHP_EOL;
		return;
	}
	if(file_exists('/etc/openvpn/dudko-web-panel/easy-rsa/vars-'.$servername)) { 
		echo 'Сервер уже существует(vars)!';
      	$log .= date("Y-m-d H:i:s").'     '.'Сервер уже существует(vars)!';
  		$log .= PHP_EOL;
		return;
	}
	if(file_exists('/etc/openvpn/'.$servername.'.conf')) { 
		echo 'Сервер уже существует(config)!';
      	$log .= date("Y-m-d H:i:s").'     '.'Сервер уже существует(config)!';
  		$log .= PHP_EOL;
		return;
	}
 
	/* настраиваем каталоги сервера */
	if(!mkdir('/etc/openvpn/dudko-web-panel/settings/'.$servername.'/', 0755, true)) {
		echo 'Не удалось создать каталог .../settings/'.$servername.'/!';
      	$log .= date("Y-m-d H:i:s").'     '.'Не удалось создать каталог .../settings/'.$servername.'/!';
  		$log .= PHP_EOL;
		return;
	}
	if(!mkdir('/etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/', 0755, true)) {
		echo 'Не удалось создать каталог .../rsa-key/'.$servername.'/!';
      	$log .= date("Y-m-d H:i:s").'     '.'Не удалось создать каталог .../rsa-key/'.$servername.'/!';
  		$log .= PHP_EOL;
		return;
	}
 
	/* конфигурационный файл генерации сертификатов*/
	$vars_file = 'export EASY_RSA="`pwd`"' . PHP_EOL .
	'export OPENSSL="openssl"' . PHP_EOL .
	'export PKCS11TOOL="pkcs11-tool"' . PHP_EOL .
	'export GREP="grep"' . PHP_EOL .
	'export KEY_CONFIG=`$EASY_RSA/whichopensslcnf $EASY_RSA`' . PHP_EOL .
	'export KEY_DIR="/etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/"' . PHP_EOL .
	'export PKCS11_MODULE_PATH="dummy"' . PHP_EOL .
	'export PKCS11_PIN="dummy"' . PHP_EOL .
	'export KEY_SIZE='.$variable['KEY_SIZE'].'' . PHP_EOL .
	'export CA_EXPIRE='.$variable['CA_EXPIRE'].'' . PHP_EOL .
	'export KEY_EXPIRE='.$variable['KEY_EXPIRE'].'' . PHP_EOL .
	'export KEY_COUNTRY="'.$variable['KEY_COUNTRY'].'"' . PHP_EOL .
	'export KEY_PROVINCE="'.$variable['KEY_PROVINCE'].'"' . PHP_EOL .
	'export KEY_CITY="'.$variable['KEY_CITY'].'"' . PHP_EOL .
	'export KEY_ORG="'.$variable['KEY_ORG'].'"' . PHP_EOL .
	'export KEY_EMAIL="'.$variable['KEY_EMAIL'].'"' . PHP_EOL .
	'export KEY_OU="'.$variable['KEY_OU'].'"' . PHP_EOL .
	'export KEY_NAME="'.$variable['KEY_NAME'].'"' . PHP_EOL .
	'export KEY_CN="'.$variable['KEY_CN'].'"' . PHP_EOL .
	'export KEY_ALTNAMES="'.$variable['KEY_ALTNAMES'].'"' . PHP_EOL .
	'echo OK: If you run ./clean-all, I will be doing a rm -rf on $KEY_DIR' . PHP_EOL;
 
	if(!file_put_contents('/etc/openvpn/dudko-web-panel/easy-rsa/vars-'.$servername, $vars_file)){
		echo 'Не удалось записать файл vars-'.$servername.'!';
      	$log .= date("Y-m-d H:i:s").'     '.'Не удалось записать файл vars-'.$servername.'!';
  		$log .= PHP_EOL;
		return;
	} 
	chmod('/etc/openvpn/dudko-web-panel/easy-rsa/vars-'.$servername, 0755);
 
	cmd_exec('ca', 'cd /etc/openvpn/dudko-web-panel/easy-rsa/ && source ./vars-'.$servername .' && ./clean-all  && ./build-ca', $callback, $err);
	unset($callback);unset($err);
	cmd_exec('server','cd /etc/openvpn/dudko-web-panel/easy-rsa/ && source ./vars-'.$servername .' && ./build-key-server server', $callback, $err);
	$result_req = substr($err[31], 0, 17); //Data Base Updated
	unset($callback);unset($err);
	cmd_exec('dh','cd /etc/openvpn/dudko-web-panel/easy-rsa/ && source ./vars-'.$servername .' && ./build-dh', $callback, $err);
	unset($callback);unset($err);
	exec('openvpn --genkey --secret /etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/ta.key', $callback);
 
	/* конфигурационный файл сервера */
	$config_file = 'mode server' .PHP_EOL .
	'tls-server' .PHP_EOL .
	'proto tcp-server' .PHP_EOL .
	'dev tap' .PHP_EOL .
	'port '.strval(1700 + $servernum).' # Порт' .PHP_EOL .
	'daemon' .PHP_EOL .
	'tls-auth /etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/ta.key 0' .PHP_EOL .
	'ca /etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/ca.crt' .PHP_EOL .
	'cert /etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/server.crt' .PHP_EOL .
	'key /etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/server.key' .PHP_EOL .
	'dh /etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/dh2048.pem' .PHP_EOL .
	'ifconfig-pool-persist /etc/openvpn/dudko-web-panel/settings/'.$servername.'/ipp.txt' .PHP_EOL .
	'ifconfig 13.'.strval($servernum).'.0.1 255.255.255.0 # Внутренний IP сервера' .PHP_EOL .
	'ifconfig-pool 13.'.strval($servernum).'.0.30 13.'.strval($servernum).'.0.255 # Пул адресов.' .PHP_EOL .
	'client-to-client' .PHP_EOL .
	'client-config-dir /etc/openvpn/dudko-web-panel/settings/'.$servername.'/' .PHP_EOL .
	'push "route-gateway 13.'.strval($servernum).'.0.1"' .PHP_EOL .
	'duplicate-cn' .PHP_EOL .
	'verb 1' .PHP_EOL .
	'cipher '.$variable['cipher'].' # Тип шифрования.' .PHP_EOL .
	'persist-key' .PHP_EOL .
	'log-append /etc/openvpn/dudko-web-panel/settings/'.$servername.'/openvpn.log # Лог-файл.' .PHP_EOL .
	'persist-tun' .PHP_EOL .
	'comp-lzo' .PHP_EOL;
 
	if(!file_put_contents('/etc/openvpn/'.$servername.'.conf', $config_file)){
		echo 'Не удалось записать файл '.$servername.'.conf!';
      	$log .= date("Y-m-d H:i:s").'     '.'Не удалось записать файл '.$servername.'.conf!';
  		$log .= PHP_EOL;
		return;
	}
	chmod('/etc/openvpn/'.$servername.'.conf', 0755);
	$serverstring = '/etc/openvpn/dudko-web-panel/rsa-key/'.$servername;
	if(($result_req == 'Data Base Updated') && file_exists($serverstring.'/ca.crt')  && file_exists($serverstring.'/ta.key')  
		&& file_exists($serverstring.'/server.crt')  && file_exists($serverstring.'/server.key')  && file_exists($serverstring.'/dh2048.pem')
		&& file_exists('/etc/openvpn/'.$servername.'.conf')){
			$result = exec('systemctl start openvpn@'.$servername, $callback);
			if($result == ""){
				cmd_exec('systemctl','systemctl enable openvpn@'.$servername, $callback, $err);
				if(substr($err[0], 0, 7) == 'Created'){
					unset($callback);unset($err);
					echo 'Сервер успешно создан!';
                  	$log .= date("Y-m-d H:i:s").'     '.'Сервер успешно создан!';
  					$log .= PHP_EOL;
					return;
				}
				unset($callback);unset($err);
				echo 'Не удалось добавить юнит в автозагрузку!';
              	$log .= date("Y-m-d H:i:s").'     '.'Не удалось добавить юнит в автозагрузку!';
  				$log .= PHP_EOL;
				return;
			}
			echo 'Не удалось запустить сервер!';
      		$log .= date("Y-m-d H:i:s").'     '.'Не удалось запустить сервер!';
      		$log .= PHP_EOL;
			return;
		}
	else {
		echo 'Произошла ошибка в конфигурации сервера!';
      	$log .= date("Y-m-d H:i:s").'     '.'Произошла ошибка в конфигурации сервера!';
      	$log .= PHP_EOL;
		RemoveServer($servername, $log2);
      	$log .= $log2;
		return;
	}
}
 
function RemoveServer($servername, &$log2){
	exec('rm -rf /etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/ y');
	sleep(2);
	exec('rm -rf /etc/openvpn/dudko-web-panel/settings/'.$servername.'/ y');
	sleep(2);
	exec('rm -rf /etc/openvpn/dudko-web-panel/easy-rsa/vars-'.$servername.' y');
	exec('rm -rf /etc/openvpn/'.$servername.'.conf y');
	exec('systemctl stop openvpn@'.$servername);
	exec('systemctl disable openvpn@'.$servername);
	echo 'Конфигурационные файлы удалены!';
  	$log2 .= date("Y-m-d H:i:s").'     '.'Конфигурационные файлы удалены!';
  	$log2 .= PHP_EOL;
	return;
}
 
exit;
?>

Для функционирования скрипта необходимо создать таблицу в MySQL:

CREATE TABLE `server_conf` (
  `Id` INT(11) NOT NULL AUTO_INCREMENT,
  `dev` VARCHAR(255) DEFAULT NULL,
  `hostname` VARCHAR(255) DEFAULT NULL,
  `cipher` VARCHAR(255) DEFAULT NULL,
  `KEY_SIZE` VARCHAR(255) DEFAULT NULL,
  `CA_EXPIRE` VARCHAR(255) DEFAULT NULL,
  `KEY_EXPIRE` VARCHAR(255) DEFAULT NULL,
  `KEY_COUNTRY` VARCHAR(255) DEFAULT NULL,
  `KEY_PROVINCE` VARCHAR(255) DEFAULT NULL,
  `KEY_CITY` VARCHAR(255) DEFAULT NULL,
  `KEY_ORG` VARCHAR(255) DEFAULT NULL,
  `KEY_EMAIL` VARCHAR(255) DEFAULT NULL,
  `KEY_OU` VARCHAR(255) DEFAULT NULL,
  `KEY_NAME` VARCHAR(255) DEFAULT NULL,
  `KEY_CN` VARCHAR(255) DEFAULT NULL,
  `KEY_ALTNAMES` VARCHAR(255) DEFAULT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
 

С начинкой типа:

INSERT INTO `server_conf` VALUES (1,'tap','vpn.sergdudko.tk','DES-EDE3-CBC','2048','3650','1095','BY','MSQ','Minsk','Name Surname Service','admin@sergdudko.tk','VPN Service','Searhei Surname Key','sergdudko.tk','VPNSERGDUDKOTK');

Собственно в начинке стандартные параметры конфигурации OpenVPN сервера. Т.к. наш скрипт консольный, аргументы задаются параметром —args и в скрипте будут выглядеть как массив argv[].
Первое что делает скрипт, это разбирает аргументы на два вида: добавить сервер и удалить сервер. После этого вызывает соответствующую функцию.
Функция CreateServer создает сертификаты и конфигурационный файл, запускает сервер и добавляет его в атозагрузку. Т.к. для генерации сертификатов необходимо выполнять некоторые инъекции в shell, что невозможно осуществить стандартной exec (аргументы задаются до выполнения команды, в процессе же выполнения они недоступны), была написана функция cmd_exec, которая общается с shell по средствам потоков.
Функция RemoveServer удаляет все файлы сервера (сертификаты, в т.ч. клиентские, конфигурационный файл, логи), останавливает его и удаляет из автозагрузке. Она также вызывается в случае ошибки при создании сервера.

Следующее это создать сертификаты клиентов. Проделал эту операцию в shell и воспроизвел в скрипте:

Показать

<?php
//php /etc/openvpn/scripts/build_client.php --args -add 1 3 test@email.by
include(__DIR__ . '/settings.php'); 
$current = 'php /etc/openvpn/scripts/build_client.php --args '.$argv[2].' '.$argv[3].' '.$argv[4];
if(isset($argv[5])){
  	$current .= ' '.$argv[5];
}
$current .= PHP_EOL;
if(!file_exists('/etc/openvpn/dudko-web-panel/')) { 
  if(!mkdir('/etc/openvpn/dudko-web-panel/', 0755, true)) {
    echo 'Не удалось создать каталог .../dudko-web-panel/!';
    exit;
  }
}
if(!is_dir('/etc/openvpn/dudko-web-panel/logs')){ mkdir('/etc/openvpn/dudko-web-panel/logs');}
$file = '/etc/openvpn/dudko-web-panel/logs/server'.$argv[3].date("Y-m-d").'.log';
if(file_exists($file)){ $current .= file_get_contents($file); }
$servername = 'server'.$argv[3];
 
if(!file_exists('/etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/')) {
    echo 'Отсутствует каталог .../rsa-key/'.$servername.'/!';
    $current .= date("Y-m-d H:i:s").'     '.'Отсутствует каталог .../rsa-key/'.$servername.'/!';
    $current .= PHP_EOL;
  	echo PHP_EOL;
  	file_put_contents($file, $current);
  	exit;
}
if(!file_exists('/etc/openvpn/dudko-web-panel/settings/'.$servername.'/')) {
    echo 'Отсутствует каталог .../settings/'.$servername.'/!';
    $current .= date("Y-m-d H:i:s").'     '.'Отсутствует каталог .../settings/'.$servername.'/!';
    $current .= PHP_EOL;
  	echo PHP_EOL;
  	file_put_contents($file, $current);
  	exit;
}
if(!file_exists('/etc/openvpn/dudko-web-panel/easy-rsa/')) {
    echo 'Отсутствует каталог .../easy-rsa/!';
    $current .= date("Y-m-d H:i:s").'     '.'Отсутствует каталог .../easy-rsa/!';
    $current .= PHP_EOL;
  	echo PHP_EOL;
  	file_put_contents($file, $current);
  	exit;
}
if(!file_exists('/etc/openvpn/dudko-web-panel/easy-rsa/vars-'.$servername)) {
    echo 'Отсутствует файл конфигурации .../easy-rsa/vars-'.$servername.'!';
    $current .= date("Y-m-d H:i:s").'     '.'Отсутствует файл конфигурации .../easy-rsa/vars-'.$servername.'!';
    $current .= PHP_EOL;
  	echo PHP_EOL;
  	file_put_contents($file, $current);
  	exit;
}
 
if($argv[2] == '-add'){
  $clientstring = '/etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/client'.$argv[4].'.';
  if(file_exists($clientstring.'key') || file_exists($clientstring.'crt')) {
      echo 'Для клиента уже создан сертификат!';
      $current .= date("Y-m-d H:i:s").'     '.'Для клиента уже создан сертификат!';
      $current .= PHP_EOL;
      echo PHP_EOL;
      file_put_contents($file, $current);
      exit;
  }
  CreateClient($argv[3], $current, $argv[4], $argv[5]);
}
if($argv[2] == '-remove'){
  $clientstring = '/etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/client'.$argv[4].'.';
  if(!file_exists($clientstring.'key') || !file_exists($clientstring.'crt')) {
      echo 'Для клиента отсутствует сертификат!';
      $current .= date("Y-m-d H:i:s").'     '.'Для клиента отсутствует сертификат!';
      $current .= PHP_EOL;
      echo PHP_EOL;
      file_put_contents($file, $current);
      exit;
  }
  RemoveClient($argv[3], $current, $argv[4]);
}
 
echo PHP_EOL;
$current .= PHP_EOL;
file_put_contents($file, $current);
exit;
 
function cmd_exec($usermail, $cmd, &$stdout, &$stderr)
{
    $outfile = tempnam(".", "cmd");
    $errfile = tempnam(".", "cmd");
    $descriptorspec = array(
        0 => array("pipe", "r"),
        1 => array("file", $outfile, "w"),
        2 => array("file", $errfile, "w")
    );
    $proc = proc_open($cmd, $descriptorspec, $pipes);
 
    if (!is_resource($proc)) return 255;
	if($usermail != ''){
		for($i=0;$i<7;$i++){ //7 переходов
			fwrite($pipes[0], "\n");
			usleep(200);
		}
      	fwrite($pipes[0], $usermail."\n");
        usleep(200);
        fwrite($pipes[0], "\n");
        usleep(200);
        fwrite($pipes[0], "\n");
        usleep(200);
        sleep(3);
        fwrite($pipes[0], "y\n");
        usleep(500);
        fwrite($pipes[0], "y\n");
	} 
    fclose($pipes[0]); 
 
    $exit = proc_close($proc);
	$stdout = file($outfile);
    $stderr = file($errfile);
 
    unlink($outfile);
    unlink($errfile);
    return $exit;
}
 
function CreateClient($servernum, &$log, $clientnum, $clientmail){
	cmd_exec($clientmail, strval('cd /etc/openvpn/dudko-web-panel/easy-rsa/ && source ./vars-server'.$servernum .' && ./build-key client'.$clientnum), $callback, $err);
  	$result_req = substr($err[31], 0, 17);
  	unset($callback);unset($err);
  	$clientstring = '/etc/openvpn/dudko-web-panel/rsa-key/server'.$servernum.'/client'.$clientnum;
  	if(($result_req == 'Data Base Updated') && file_exists($clientstring.'.key') && file_exists($clientstring.'.crt')){
      	echo 'Сертификаты успешно созданы!';
      	exec('systemctl restart openvpn@server'.$servernum);
      	$log .= date("Y-m-d H:i:s").'     '.'Сертификаты успешно созданы!';
      	$log .= PHP_EOL;
    	return;
    }
  	echo 'Ошибка создания сертификатов!';
  	$log .= date("Y-m-d H:i:s").'     '.'Ошибка создания сертификатов!';
  	$log .= PHP_EOL;
  	return;
}
 
function RemoveClient($servernum, &$log, $clientnum){
  	$clientstring = '/etc/openvpn/dudko-web-panel/rsa-key/server'.$servernum;
  	if(!file_exists($clientstring.'/crl.pem')){
    	$new_conf = file_get_contents('/etc/openvpn/server'.$servernum.'.conf');
      	$new_conf = $new_conf . 'crl-verify ' . $clientstring.'/crl.pem' . PHP_EOL;
    }
	cmd_exec('', strval('cd /etc/openvpn/dudko-web-panel/easy-rsa/ && source ./vars-server'.$servernum .' && ./revoke-full client'.$clientnum), $callback, $err);
  	$result_req = substr($err[2], 0, 17);
  	unset($callback);unset($err);
  	if(($result_req == 'Data Base Updated') && file_exists($clientstring.'/crl.pem')){
      	if(isset($new_conf)){
        	file_put_contents('/etc/openvpn/server'.$servernum.'.conf', $new_conf);
        }
      	echo 'Сертификат успешно отозван!';
      	exec('systemctl restart openvpn@server'.$servernum);
      	$log .= date("Y-m-d H:i:s").'     '.'Сертификат успешно отозван!';
      	$log .= PHP_EOL;
    	return;
    }
  	echo 'Ошибка отзыва сертификата!';
  	$log .= date("Y-m-d H:i:s").'     '.'Ошибка отзыва сертификата!';
  	$log .= PHP_EOL; 
  	return;
}
exit;
?>

Тут все аналогично скрипту создания сервера, за исключением двух вещей. Конфигурационный файл клиента мы не создаем, т.к. нет смысла захламлять сервер, будем генерировать его налету. А также, при удалении клиента нам необходимо добавить СОС(список отзыва сертификатов) и добавить строку с настройкой СОС в конфигурационный файл сервера. При этом менять конфигурационный файл сервера нужно только в том случае, если до этого файл СОС не был создан.
Собственно был и обходной путь (мне показался костыльным) — создать клиента и сразу отозвать сертификат, после чего сгенерировать конфиг сервера и уже его запускать. Все дела в том, что если обозначить в файле конфигурации сервера настройку СОС, а сам файл не сгенерировать(как сгенерировать пустой я не нашел, а через «блокнот» не прокатило), сервер не запустится.
Для работы обоих скриптов нам нужны настройки базы данных и даты(если в php.ini не настроена), я, как обычно, вывел эти настройки в отдельный скрипт:

<?php
 
$db_ipaddr = '127.0.0.1'; 	//адрес БД
$db_user = '*******';		//пользователь БД
$db_pass = '*******'; 		//пароль
$db_name = '******';         //имя БД
 
date_default_timezone_set('Europe/Minsk');
header('Content-Type: text/text; charset=utf-8');
 
?>

Сертификаты готовы, сервер создан. Но клиент должен как-то получить свои сертификаты. Наиболее популярный способ(после скачать) — отправить на email. К тому же отправить на email удобней, с т.з. того, что сертификаты генерируются на консольном сервере и качать особо неоткуда.
Для работы с электронной почтой использую класс SendMailSmtpClass.php, автор класса указан в его составе, расписывать работу не буду — просто приведу исходник:

Показать

<?php
/**
* SendMailSmtpClass
* 
* Класс для отправки писем через SMTP с авторизацией
* Может работать через SSL протокол
* Тестировалось на почтовых серверах yandex.ru, mail.ru и gmail.com
* 
* @author Ipatov Evgeniy <admin@ipatov-soft.ru>
* @version 1.0
*/
class SendMailSmtpClass {
 
    /**
    * 
    * @var string $smtp_username - логин
    * @var string $smtp_password - пароль
    * @var string $smtp_host - хост
    * @var string $smtp_from - от кого
    * @var integer $smtp_port - порт
    * @var string $smtp_charset - кодировка
    *
    */   
    public $smtp_username;
    public $smtp_password;
    public $smtp_host;
    public $smtp_from;
    public $smtp_port;
    public $smtp_charset;
 
    public function __construct($smtp_username, $smtp_password, $smtp_host, $smtp_from, $smtp_port = 25, $smtp_charset = "utf-8") {
        $this->smtp_username = $smtp_username;
        $this->smtp_password = $smtp_password;
        $this->smtp_host = $smtp_host;
        $this->smtp_from = $smtp_from;
        $this->smtp_port = $smtp_port;
        $this->smtp_charset = $smtp_charset;
    }
 
    /**
    * Отправка письма
    * 
    * @param string $mailTo - получатель письма
    * @param string $subject - тема письма
    * @param string $message - тело письма
    * @param string $headers - заголовки письма
    *
    * @return bool|string В случаи отправки вернет true, иначе текст ошибки    *
    */
    function send($mailTo, $subject, $message, $headers) {
        $contentMail = "Date: " . date("D, d M Y H:i:s") . " UT\r\n";
        $contentMail .= 'Subject: =?' . $this->smtp_charset . '?B?'  . base64_encode($subject) . "=?=\r\n";
        $contentMail .= $headers . "\r\n";
        $contentMail .= $message . "\r\n";
 
        try {
            if(!$socket = @fsockopen($this->smtp_host, $this->smtp_port, $errorNumber, $errorDescription, 30)){
                throw new Exception($errorNumber.".".$errorDescription);
            }
            if (!$this->_parseServer($socket, "220")){
                throw new Exception('Connection error');
            }
 
			$server_name = $_SERVER["SERVER_NAME"];
            fputs($socket, "HELO $server_name\r\n");
            if (!$this->_parseServer($socket, "250")) {
                fclose($socket);
                throw new Exception('Error of command sending: HELO');
            }
 
            fputs($socket, "AUTH LOGIN\r\n");
            if (!$this->_parseServer($socket, "334")) {
                fclose($socket);
                throw new Exception('Autorization error');
            }
 
 
 
            fputs($socket, base64_encode($this->smtp_username) . "\r\n");
            if (!$this->_parseServer($socket, "334")) {
                fclose($socket);
                throw new Exception('Autorization error');
            }
 
            fputs($socket, base64_encode($this->smtp_password) . "\r\n");
            if (!$this->_parseServer($socket, "235")) {
                fclose($socket);
                throw new Exception('Autorization error');
            }
 
            fputs($socket, "MAIL FROM: <".$this->smtp_username.">\r\n");
            if (!$this->_parseServer($socket, "250")) {
                fclose($socket);
                throw new Exception('Error of command sending: MAIL FROM');
            }
 
			$mailTo = ltrim($mailTo, '<');
			$mailTo = rtrim($mailTo, '>');
            fputs($socket, "RCPT TO: <" . $mailTo . ">\r\n");     
            if (!$this->_parseServer($socket, "250")) {
                fclose($socket);
                throw new Exception('Error of command sending: RCPT TO');
            }
 
            fputs($socket, "DATA\r\n");     
            if (!$this->_parseServer($socket, "354")) {
                fclose($socket);
                throw new Exception('Error of command sending: DATA');
            }
 
            fputs($socket, $contentMail."\r\n.\r\n");
            if (!$this->_parseServer($socket, "250")) {
                fclose($socket);
                throw new Exception("E-mail didn't sent");
            }
 
            fputs($socket, "QUIT\r\n");
            fclose($socket);
        } catch (Exception $e) {
            return  $e->getMessage();
        }
        return true;
    }
 
    private function _parseServer($socket, $response) {
        while (@substr($responseServer, 3, 1) != ' ') {
            if (!($responseServer = fgets($socket, 256))) {
                return false;
            }
        }
        if (!(substr($responseServer, 0, 3) == $response)) {
            return false;
        }
        return true;
 
    }
}

Ну и собственно мой скрипт отправки файлов:

Показать

<?php
// php /etc/openvpn/scripts/cert_send_toemail.php --args 1 1 admin@sergdudko.tk
$servername = 'server'.$argv[2];
$servernum = $argv[2];
$usermail = $argv[4];
$certificat = 'client'.$argv[3];
 
/* загружаем настройки и настраиваем логгирование  */
include(__DIR__ . '/settings.php'); 
$current = 'php /etc/openvpn/scripts/cert_send_toemail.php --args '.$argv[2].' '.$argv[3].' '.$argv[4] . PHP_EOL;
if(!file_exists('/etc/openvpn/dudko-web-panel/')) { 
  if(!mkdir('/etc/openvpn/dudko-web-panel/', 0755, true)) {
    echo 'Не удалось создать каталог .../dudko-web-panel/!';
    exit;
  }
}
if(!is_dir('/etc/openvpn/dudko-web-panel/logs/')){ mkdir('/etc/openvpn/dudko-web-panel/logs/');}
$file = '/etc/openvpn/dudko-web-panel/logs/'.$servername.date("Y-m-d").'.log';
if(file_exists($file)){ $current .= file_get_contents($file); }
/* проверяем, что аргументы получены, требуемый сертификат имеется на сервере и  не принадлежит серверу */ 
if(!isset($usermail) || !isset($certificat)|| !isset($servername)){
  	echo 'Некорректные параметры для отправки!';
  	exit;
} 
if(!file_exists('/etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/'.$certificat.'.key') || !file_exists('/etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/'.$certificat.'.crt')){ 
  	echo 'Сертификаты для данного клиента отсутствуют!';
  	exit; 
}
 
/* получаем переменные из БД для авторизации на почтовом сервере */
$db_connect = new mysqli($db_ipaddr, $db_user, $db_pass, $db_name); 
if ($db_connect->connect_errno) {
  	$current .= date("Y-m-d H:i:s").'     '."Не удалось подключиться к MySQL:" . $db_connect->connect_error;
  	$current .= PHP_EOL;
    file_put_contents($file, $current);  
  	exit;
} 
$db_connect->query("SET NAMES utf8"); 
$result = $db_connect->query("SELECT * FROM `post_conf`");
while($row = $result->fetch_assoc()){
  	$variable[$row['name']] = $row['value'];
}
$db_connect->close();
 
/* создаем архив с файлами  */
//генерация пароля на архив
$passwd = ''; 
$array = array_merge(range('A','Z'),range('a','z'),range('0','9')); 
$c = count($array); 
$longpass = rand(70,100);
for($i=0;$i<$longpass;$i++) {$passwd .= $array[rand(0,$c-1)];}
//создадим временный файл в памяти
$zip_file = '/tmp/zip-cert-'.md5(time()).'.zip';
$config_file = '/tmp/'.$certificat.'.ovpn';
file_put_contents($config_file, generate_config($servernum, $certificat)); 
//вариант через shell(с паролем)
$files_to_zip = '';
if(file_exists('/etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/'.$certificat.'.key')){ 
  $files_to_zip .= $certificat.'.key ';
}
if(file_exists('/etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/'.$certificat.'.crt')){ 
  $files_to_zip .= $certificat.'.crt ';
}
if(file_exists('/etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/ta.key')){ 
  $files_to_zip .= 'ta.key ';
}
if(file_exists('/etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/ca.crt')){ 
  $files_to_zip .= 'ca.crt ';
}
$answr = explode(' ', exec('cd  /etc/openvpn/dudko-web-panel/rsa-key/'.$servername.'/ && zip '.$zip_file.' '.$files_to_zip.' -P '.$passwd));
if ($answr[2] != 'adding:') {
    $current .= date("Y-m-d H:i:s").'     '."Ошибка создания архива";
  	$current .= PHP_EOL;
  	unlink($zip_file);
	unlink($config_file);
  	file_put_contents($file, $current);  
  	exit;
}
if(file_exists($config_file)){ 
  $files_to_zip = $certificat.'.ovpn';
}
$answr = explode(' ', exec('cd  /tmp/ && zip '.$zip_file.' '.$files_to_zip.' -P '.$passwd));
if ($answr[2] != 'adding:') {
    $current .= date("Y-m-d H:i:s").'     '."Ошибка добавления конфиг-файла в архив";
  	$current .= PHP_EOL;
  	unlink($zip_file);
	unlink($config_file);
  	file_put_contents($file, $current);  
  	exit;
}
 
/* отправка архива на почту  */
$_SERVER["SERVER_NAME"] = 'sergdudko.tk';
require_once ("/etc/openvpn/scripts/SendMailSmtpClass.php"); // подключаем класс
$mailSMTP = new SendMailSmtpClass($variable['email_user'], $variable['email_pass'], $variable['email_host'], $variable['email_name'], $variable['email_port']); // создаем экземпляр класса
$headers= "MIME-Version: 1.0\r\n"; 
$un = rand(1000,9999);
//$headers .= "Content-Type: multipart/alternative;boundary=\"----------".$un."\"".PHP_EOL; // кодировка письма
$headers .= "Content-Type: multipart/mixed;boundary=\"----------".$un."\"".PHP_EOL; // кодировка письма
$headers .= "Content-Language: ru". PHP_EOL;
$headers .= 'From: '.$variable['email_name'].' <'.$variable['email_user'].'>'.PHP_EOL; // от кого письмо
$headers .= 'To: '.$usermail.PHP_EOL; // от кого письмо
$message = 	"------------".$un. PHP_EOL .
  			"Content-Type:text/plain; charset=utf-8". PHP_EOL .
  			"Content-Transfer-Encoding: 8bit". PHP_EOL .
  			"Архив с сертификатами во вложении, пароль на архив был предоставлен на сайте.". PHP_EOL .
  			"Обращаю внимание на то, что длинна не все архиваторы поддерживают заданную длинну пароля. Если вы получили сообщение о том, что пароль не верен - попробуйте другим архиватором.". PHP_EOL .
"------------".$un. PHP_EOL .
  			"Content-Type: application/octet-stream;name=\"file.zip\"". PHP_EOL .
  			"Content-Transfer-Encoding:base64". PHP_EOL .
  			"Content-Disposition:attachment;filename=\"file.zip\"". PHP_EOL .chunk_split(base64_encode(file_get_contents($zip_file))). PHP_EOL ."
------------".$un."--";
$subject = 'Ключи доступа'.PHP_EOL;
$result =  $mailSMTP->send($usermail, $subject, $message, $headers); // отправляем письмо
if($result === true){
  echo 'Сертификаты были успешно отправлены на '. $usermail . ', пароль на архив:' . PHP_EOL . $passwd .PHP_EOL; 
  $current .= date("Y-m-d H:i:s").'     '.'Сертификаты были успешно отправлены на '. $usermail .PHP_EOL;
}else{
  $current .= date("Y-m-d H:i:s").'     '.'Попытка отправить сертификаты через резервный ящик!'.PHP_EOL;
  $mailSMTP_reserve = new SendMailSmtpClass($variable['email_user_reserve'], $variable['email_pass_reserve'], $variable['email_host_reserve'], $variable['email_name'], $variable['email_port_reserve']); // создаем экземпляр класса
  $result2 =  $mailSMTP_reserve->send($usermail, $subject, $message, $headers); // отправляем письмо с резервного ящика
  if($result2 === true){
    echo 'Сертификаты были успешно отправлены на '. $usermail . ', пароль на архив:' . PHP_EOL . $passwd .PHP_EOL; 
    $current .= date("Y-m-d H:i:s").'     '.'Сертификаты были успешно отправлены на '. $usermail .PHP_EOL;
  }else{
    echo 'Ошибка отправки сертификатов на '. $usermail . '!' . PHP_EOL; 
    $current .= date("Y-m-d H:i:s").'     '.'Ошибка отправки сертификатов на '. $usermail . '!' . PHP_EOL;    
  }
}
 
/* Удаляем архив и пишем содержимое лога в файл */
unlink($zip_file);
unlink($config_file);
$current .= PHP_EOL;
file_put_contents($file, $current);
 
function generate_config($server_num, $certificat){
  	include(__DIR__ . '/settings.php'); 
  	$db_connect = new mysqli($db_ipaddr, $db_user, $db_pass, $db_name); 
    if ($db_connect->connect_errno) {
        $current .= date("Y-m-d H:i:s").'     '."Не удалось подключиться к MySQL:" . $db_connect->connect_error;
        $current .= PHP_EOL;
        file_put_contents($file, $current);  
        exit;
    } 
    $db_connect->query("SET NAMES utf8"); 
    $result = $db_connect->query("SELECT * FROM `server_conf` WHERE Id LIKE ".$server_num.""); 
    while($row = $result->fetch_assoc()){
        $variable_func = $row;
      	$variable_func['port'] = 1700+intval($row['Id']);
    }
    $db_connect->close();
  	$conf_file = '';
    $conf_file .= 'tls-client'.PHP_EOL;
    $conf_file .= 'proto tcp-client'.PHP_EOL;
  	$conf_file .= 'remote '.$variable_func['hostname'].PHP_EOL;
  	$conf_file .= 'dev '.$variable_func['dev'].PHP_EOL;
	$conf_file .= 'port '.$variable_func['port'].PHP_EOL;
  	$conf_file .= 'pull '.PHP_EOL;
    $conf_file .= 'tls-auth ta.key 1'.PHP_EOL;
  	$conf_file .= 'ca ca.crt'.PHP_EOL;
	$conf_file .= 'cert '.$certificat.'.crt'.PHP_EOL;
  	$conf_file .= 'key '.$certificat.'.key'.PHP_EOL;
  	$conf_file .= 'cipher '.$variable_func['cipher'].PHP_EOL;
  	$conf_file .= 'comp-lzo'.PHP_EOL;
  	return $conf_file;
}
exit;
?>

Для его работы нужно создать настройки подключения в MySQL(использование БД оправдано, т.к. в перспективе проект думаю дорабатывать и сделать интерфейс):

CREATE TABLE `post_conf` (
  `name` VARCHAR(255) NOT NULL DEFAULT '',
  `value` VARCHAR(255) DEFAULT NULL,
  PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
 

Мой скрипт использует два адреса, основной и резервный(если через один отправить не удалось — пробуем второй). Также он создает конфигурационный файл клиента на лету + упаковывает все файлы в архив. Сама же отправка вложений по электронной почте выглядит так: обязательный заголовок

Content-Type: multipart/mixed;boundary="----------111"

где ———-111 разделитель содержимого. Тело письма делится данным разделителем на в трех местах (начало текста, конец текста, конец вложения). Т.е. по сути у нас имеется две части письма с различными заголовками (см. код выше). Протестировано на основных публичных серверах, типа gmail. В керио, например, такие файлы не пропускает, но это опустим.

Для обслуживания этих трех скриптов создал отдельный, который будет приводить в порядок аргументы из консоли(т.е. их можно будет задавать в любом порядке) и будет изменять файл настроек базы данных и часового пояса. Также этот скрипт будет запускаться из консоли прямой командой(т.е. php перед указанием скрипта можно опустить).

Показать

#!/usr/bin/env php
<?php
if(isset($argv)){
  for($i=0;$i<count($argv);$i++){
      if($argv[$i] == '-help'){
          helper();
      }
      if($argv[$i] == '-settings'){
          settings($argv);
      }
      $massive = explode(':', $argv[$i]);
      if(isset($massive[0]) && isset($massive[1])) {
      	  $command[$massive[0]] = $massive[1];
  	  }
  }
}
 
if(isset($command['build'])){
    $exec = 'php /etc/openvpn/scripts/build_'.$command['build'].'.php --args ';
    if(!isset($command['server']) || !isset($command['com'])){
		helper();
    }
    if($command['build'] == 'server'){
        $exec .= '-'.$command['com'] . ' ' . $command['server'];
    }
    if($command['build'] == 'client'){
      	if(!isset($command['client'])){
            helper();
        }
      	if($command['com'] != 'remove'){
          	if(!isset($command['email'])){
              	helper();
            }
        }
     	$exec .= '-'.$command['com'] . ' ' . $command['server'] . ' ' . $command['client'];
      	if(isset($command['email'])){
          	 $exec .= ' ' . $command['email'];
        }
    }
}
 
if(isset($command['send'])){
  if($command['send'] == 'toemail'){
    	if(!isset($command['server']) || !isset($command['email']) || !isset($command['client'])){
            helper();
        }
        $exec = 'php /etc/openvpn/scripts/cert_send_'.$command['send'].'.php --args '.$command['server'].' '.$command['client'].' '.$command['email'].'';
  }
}
 
if(isset($exec)){
	exec($exec, $callback);
  	for($i=0;$i<count($callback);$i++){
      	echo $callback[$i];
      	if(($i+1)<count($callback)){
          	echo PHP_EOL;
        }
    }
  	echo PHP_EOL;
} else {
  	helper();
}
 
function helper(){
    echo 'Доступные аргументы:' .PHP_EOL;
  	echo 'Вызов справки: -help' .PHP_EOL;
    echo 'Создать/удалить openvpn сервер: build:server com:add(или remove) server:1(численный номер сервера)' .PHP_EOL;
    echo 'Создать/удалить сертификаты клиента: build:client com:add(или remove) server:1(численный номер сервера, к которому будет привязан клиент)  email:admin@sergdudko.tk  client:1(численный номер клиентского сертификата)' .PHP_EOL;
    echo 'Отправить сертификаты клиента на email: send:toemail(отправка на email) server:1(численный номер сервера, к которому будет привязан клиент)  email:admin@sergdudko.tk  client:1(численный номер клиентского сертификата)' .PHP_EOL;
  	echo 'Для настройки программы, наберите -settings' .PHP_EOL;
    echo PHP_EOL;
    exit;
}
 
function settings($argv){
   for($i=0;$i<count($argv);$i++){
      if($argv[$i] != '-settings'){
          $massive = explode(':', $argv[$i]);
          if(isset($massive[0]) && isset($massive[1])) {
              $command[$massive[0]] = $massive[1];
          }
      }
   }
   $settings_file = "/etc/openvpn/scripts/settings.php";
   if(!file_exists($settings_file)){
     	echo 'Файл с настройками не найден!' . PHP_EOL;
     	exit;
   }
   $handle = @fopen($settings_file, "r");
   if ($handle) {
     	$i=0;
        while (($buffer = fgets($handle, 4096)) !== false) {
            $str[$i] = $buffer;
            $i++;
        }
        if (!feof($handle)) {
            echo "Error: unexpected fgets() fail\n";
        }
   		fclose($handle);
   }
   $current = '';
   $flag = 0;
   for($i=0;$i<count($str);$i++){
       if(isset($command['db_host']) && (substr($str[$i], 0, 10) == '$db_ipaddr')){
			$str[$i] = '$db_ipaddr = \''.$command['db_host'].'\';       //адрес БД' . PHP_EOL;
         	$flag = 1;
       }  
       if(isset($command['db_name']) && (substr($str[$i], 0, 8) == '$db_name')){
			$str[$i] = '$db_name = \''.$command['db_name'].'\';         //имя БД' . PHP_EOL;
            $flag = 1;
       }  
       if(isset($command['db_user']) && (substr($str[$i], 0, 8) == '$db_user')){
			$str[$i] = '$db_user = \''.$command['db_user'].'\';         //пользователь БД' . PHP_EOL;
         	$flag = 1;
       } 
       if(isset($command['db_pass']) && (substr($str[$i], 0, 8) == '$db_pass')){
			$str[$i] = '$db_pass = \''.$command['db_pass'].'\';         //пароль' . PHP_EOL;
         	$flag = 1;
       }  
       if(isset($command['timezone']) && (substr($str[$i], 0, 25) == 'date_default_timezone_set')){
			$str[$i] = 'date_default_timezone_set(\''.$command['timezone'].'\');' . PHP_EOL;
         	$flag = 1;
       }
       $current .= $str[$i];
   }
   if($flag == 1) {
     	if(file_put_contents($settings_file, $current)){
          	echo 'Настройки изменены!' . PHP_EOL;
        }
   } else {
     	echo 'Параметры: db_host:hostname(адрес БД MySQL или ip) db_name:name(имя БД) db_user:user(пользователь БД) db_pass:password(пароль пользователя БД) encoding:Europe/Minsk(таймзона)' . PHP_EOL;
     	echo '-settings обязательный префикс, лишние параметры можно пропустить' . PHP_EOL;
   }
   exit;
}
 
exit;
?>

Програмная часть минимально готова, вернемся к упаковке пакета и его цифровой подписи(собственно подписывается он автоматически). Для упаковки програмной части в rpm положим програмную часть в папку dudko-web-panel-0.0.1 и упакуем в одноименный tar.gz архив (это важно).
Далее архив необходимо положить в /home/user/rpmbuild/SOURCES/. В /home/user/ cjplflbv afqk .rpmmacros с содержимым:

%dist .fc7
%packager       Name Surname <admin@sergdudko.tk>
%vendor         vpn.sergdudko.tk
%_signature gpg
%_gpg_name Name Surname (tel: <+375000000000 reserved: <test@gmail.com>) <admin@sergdudko.tk>

По содержимому расписывать не буду, все и так ясно: версия ОС, упаковщик, цифровая подпись.

Можно приступать к созданию пакета, которое собственно заключается в написании spec-файла. В папке /home/name/rpmbuild/SPECS/ файл dudko-web-panel.spec:

Показать

Summary:        OpenVPN WEB Panel
Name:           dudko-web-panel
Version:        0.0.2
Release:        1
License:        GPL
URL:            https://sergdudko.tk
Group:          System/Configuration/Networking
Source0:        %{name}-%{version}.tar.gz
BuildArch:      noarch
BuildRoot:      %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
Requires:  php
Requires:  openvpn
Requires:  easy-rsa
 
%description
This is Web panel for OpenVPN by Name Surname.
 
%prep
%setup -q
 
%build
 
%install
rm -rf %{buildroot}
install -d %{buildroot}%{_datadir}/%{name}
install SendMailSmtpClass.php %{buildroot}%{_datadir}/%{name}
install build_client.php %{buildroot}%{_datadir}/%{name}
install build_server.php %{buildroot}%{_datadir}/%{name}
install cert_send_toemail.php %{buildroot}%{_datadir}/%{name}
install core_v1.php %{buildroot}%{_datadir}/%{name}
install settings.php %{buildroot}%{_datadir}/%{name}
install man.md %{buildroot}%{_datadir}/%{name}
 
%post
if ! [ -d /etc/openvpn/ ]; then
    mkdir -p /etc/openvpn/
fi
if ! [ -d /etc/openvpn/scripts/ ]; then
    mkdir -p /etc/openvpn/scripts/
fi
cp %{_datadir}/%{name}/SendMailSmtpClass.php /etc/openvpn/scripts/SendMailSmtpClass.php
cp %{_datadir}/%{name}/build_client.php /etc/openvpn/scripts/build_client.php
cp %{_datadir}/%{name}/build_server.php /etc/openvpn/scripts/build_server.php
cp %{_datadir}/%{name}/cert_send_toemail.php /etc/openvpn/scripts/cert_send_toemail.php
cp %{_datadir}/%{name}/core_v1.php /usr/bin/dudko-web-panel
cp %{_datadir}/%{name}/settings.php /etc/openvpn/scripts/settings.php
rm -rf %{buildroot}%{_datadir}/%{name}/
echo Успешно установлено!
 
%postun
rm -rf /etc/openvpn/scripts/
rm -rf /usr/bin/dudko-web-panel
echo Успешно удалено!
 
%files 
%dir %{_datadir}/%{name}
%defattr(0755,root,root)
%{_datadir}/%{name}/SendMailSmtpClass.php 
%{_datadir}/%{name}/build_client.php 
%{_datadir}/%{name}/build_server.php 
%{_datadir}/%{name}/cert_send_toemail.php 
%{_datadir}/%{name}/core_v1.php 
%{_datadir}/%{name}/settings.php 
%defattr(0644,root,root) 
%doc %{_datadir}/%{name}/man.md 
 
%clean
rm -rf %{buildroot}
 
%changelog
* Tue Dec 22 2017 Name Surname
- Initial build
 

Summary — публичное наименование
Name — имя пакета
Version — версия пакета
Release — релиз пакета
License — лицензия
URL — ссылка на сайт
Group — группа пакета, выбирается из стандартных
## Список возможных (допустимых) групп:
## Archiving/Backup
## Archiving/Cd burning
## Archiving/Compression
## Archiving/Other
## Books/Computer books
## Books/Faqs
## Books/Howtos
## Books/Literature
## Books/Other
## Communications
## Databases
## Development/C
## Development/C++
## Development/Databases
## Development/Debug
## Development/GNOME and GTK+
## Development/Java
## Development/KDE and Qt
## Development/Kernel
## Development/Other
## Development/Perl
## Development/PHP
## Development/Python
## Development/Ruby
## Development/X11
## Editors
## Education
## Emulators
## File tools
## Games/Adventure
## Games/Arcade
## Games/Boards
## Games/Cards
## Games/Other
## Games/Puzzles
## Games/Sports
## Games/Strategy
## Graphical desktop/Enlightenment
## Graphical desktop/FVWM based
## Graphical desktop/GNOME
## Graphical desktop/Icewm
## Graphical desktop/KDE
## Graphical desktop/Other
## Graphical desktop/Sawfish
## Graphical desktop/WindowMaker
## Graphical desktop/Xfce
## Graphics
## Monitoring
## Networking/Chat
## Networking/File transfer
## Networking/IRC
## Networking/Instant messaging
## Networking/Mail
## Networking/News
## Networking/Other
## Networking/Remote access
## Networking/WWW
## Office
## Publishing
## Sciences/Astronomy
## Sciences/Biology
## Sciences/Chemistry
## Sciences/Computer science
## Sciences/Geosciences
## Sciences/Mathematics
## Sciences/Other
## Sciences/Physics
## Shells
## Sound
## System/Base
## System/Cluster
## System/Configuration/Boot and Init
## System/Configuration/Hardware
## System/Configuration/Networking
## System/Configuration/Other
## System/Configuration/Packaging
## System/Configuration/Printing
## System/Fonts/Console
## System/Fonts/True type
## System/Fonts/Type1
## System/Fonts/X11 bitmap
## System/Internationalization
## System/Kernel and hardware
## System/Libraries
## System/Printing
## System/Servers
## System/X11
## Terminals
## Text tools
## Toys
## Video
Source0 — имя архива с приложением
BuildArch — требуемая архитектура ОС
BuildRoot — папка для распаковки пакета
Requires — требовать данный пакет, в противном случае не установится

секция %description — описание пакета
секция %prep — подготовка пакета к сборке, обычно включает %setup -q и %build (распаковку Source0)
секция %install — установка пакета, можно использовать bash, но обязательно должна быть минимум одна команда install
секция %post — постустановочная обработка пакета, например у меня bash раскладывает по папкам
секция %postun — скрипт, выполняемый после удаления пакета(в моем случае вычищает все созданные пакетом файлы, которые я разложил в %post)
секция %files — список файлов для упаковки в пакет
секция %clean — очистка временного окружения сборки
секция %changelog — лог установки

После создания spec-файла можно упаковать наши исходники в rpm командой (предварительно перейдя в каталог со спецфайлом или прописав полный путь в нижеследующей команде)

rpmbuild -ba dudko-web-panel.spec 

Наш пакет будет в папке ../RPMS/noarch/

Проверим цифровую подпись командой

rpm -K ../RPMS/noarch/dudko-web-panel-0.0.2-1.noarch.rpm

Т.к. скрипт, который обслуживает все приложение размещен в /usr/bin/dudko-web-panel, то из консоли он будет запускаться командой dudko-web-panel

Протестировано на CentOS 7.1-7.4
php 5.4, 7.0
openvpn-2.4.4-1.el7.x86_64
easy-rsa-2.2.2-1.el7.noarch

Пакет:
dudko-web-panel.tar

GPG-ключ
RPM-GPG-KEY