|
#!/usr/local/bin/php -f
|
|
<?php
|
|
/*
|
|
* ec2_setup.php
|
|
*
|
|
* part of pfSense (https://www.pfsense.org)
|
|
* Copyright (c) 2016-2023 Rubicon Communications, LLC (Netgate)
|
|
* All rights reserved.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
require_once("globals.inc");
|
|
require_once("config.inc");
|
|
require_once("auth.inc");
|
|
require_once("interfaces.inc");
|
|
require_once("certs.inc");
|
|
require_once("openvpn.inc");
|
|
|
|
function retrieveMetaData($url) {
|
|
if (!$url)
|
|
return;
|
|
|
|
$curl = curl_init();
|
|
|
|
/* first get the instance token which we will use to
|
|
authenticate the subsequent call */
|
|
|
|
$token_url = "http://169.254.169.254/latest/api/token";
|
|
$headers = array (
|
|
'X-aws-ec2-metadata-token-ttl-seconds: 10' );
|
|
curl_setopt($curl, CURLOPT_URL, $token_url);
|
|
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers );
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true );
|
|
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PUT" );
|
|
curl_setopt($curl, CURLOPT_FAILONERROR, true);
|
|
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 15);
|
|
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
|
|
$token = curl_exec($curl);
|
|
|
|
/* now build the 'real' request and send it along with the
|
|
token for authentication */
|
|
|
|
$headers = array (
|
|
'X-aws-ec2-metadata-token: '.$token );
|
|
curl_setopt($curl, CURLOPT_URL, $url);
|
|
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers );
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true );
|
|
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "GET" );
|
|
curl_setopt($curl, CURLOPT_FAILONERROR, true);
|
|
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 15);
|
|
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
|
|
$metadata = curl_exec($curl);
|
|
curl_close($curl);
|
|
|
|
return($metadata);
|
|
}
|
|
|
|
|
|
function retrieveSSHKey() {
|
|
global $g;
|
|
|
|
if ($g['default-config-flavor'] == "openstack-csm") {
|
|
$url = "http://169.254.169.254/latest/meta-data/public-ipv4";
|
|
} else {
|
|
$url = "http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key";
|
|
}
|
|
return(retrieveMetaData($url));
|
|
}
|
|
|
|
function retrieveUserData() {
|
|
$url = "http://169.254.169.254/latest/user-data/";
|
|
$user_data = retrieveMetaData($url);
|
|
|
|
if (!$user_data)
|
|
return;
|
|
|
|
/* userdata is formatted like this:
|
|
foo1=bar1:foo2=bar2:...:fooN=barN
|
|
what, were you raised in a barN? */
|
|
|
|
$kv_pairs = explode(':', $user_data);
|
|
foreach ($kv_pairs as $pair) {
|
|
list($key, $value) = explode("=", $pair, 2);
|
|
$ud[$key] = $value;
|
|
}
|
|
|
|
return($ud);
|
|
}
|
|
|
|
function retrievePublicIP() {
|
|
$wanintf = get_real_wan_interface();
|
|
$macaddr = get_interface_mac($wanintf);
|
|
if (!$macaddr)
|
|
return;
|
|
|
|
$url = "http://169.254.169.254/latest/meta-data/network/interfaces/macs/$macaddr/public-ipv4s";
|
|
$public_ipv4 = retrieveMetaData($url);
|
|
|
|
if (is_ipaddrv4($public_ipv4)) {
|
|
$natipfile = "/var/db/natted_{$wanintf}_ip";
|
|
file_put_contents($natipfile, $public_ipv4);
|
|
return($public_ipv4);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
function generateRandomPassword($length = 15) {
|
|
/* get some random bytes. use them as offsets into the space of
|
|
printable ascii characters. 32-126 is the printable characters.
|
|
Omit 32 itself since it might be confusing if there is a space
|
|
in the password.
|
|
*/
|
|
|
|
$range_size = 126 - 33 + 1;
|
|
$random_bytes = str_split(openssl_random_pseudo_bytes($length));
|
|
|
|
for ($i = 0; $i < $length; $i++) {
|
|
|
|
$offset = ord($random_bytes[$i]) % $range_size;
|
|
$password .= chr(33 + $offset);
|
|
|
|
}
|
|
|
|
return $password;
|
|
}
|
|
|
|
function addCA() {
|
|
global $config;
|
|
if (!is_array($config['ca']))
|
|
$config['ca'] = array();
|
|
|
|
$a_ca = &$config['ca'];
|
|
|
|
$ca_cfg['keylen'] = 2048;
|
|
$ca_cfg['digest_alg'] = 'sha256';
|
|
$ca_cfg['lifetime'] = 3650;
|
|
|
|
$dn['countryName'] = 'US';
|
|
$dn['stateOrProvinceName'] = 'TX';
|
|
$dn['localityName'] = 'Austin';
|
|
$dn['organizationName'] = 'Netgate VPN';
|
|
$dn['emailAddress'] = 'email';
|
|
$dn['commonName'] = 'Netgate VPN CA';
|
|
|
|
$ca = array();
|
|
$ca['refid'] = uniqid();
|
|
$ca['descr'] = 'Netgate Auto VPN CA';
|
|
|
|
if (!ca_create($ca, $ca_cfg['keylen'], $ca_cfg['lifetime'], $dn, $ca_cfg['digest_alg'])) {
|
|
$ssl_errs = 0;
|
|
while ($ssl_err = openssl_error_string()) {
|
|
$ssl_errs++;
|
|
$last_ssl_err = $ssl_err;
|
|
}
|
|
if ($ssl_errs) {
|
|
echo "Errors creating CA cert: $last_ssl_err\n";
|
|
return;
|
|
}
|
|
}
|
|
|
|
$a_ca[] = $ca;
|
|
return($ca['refid']);
|
|
}
|
|
|
|
function addServerCert($caref) {
|
|
global $config;
|
|
|
|
if (!is_array($config['cert']))
|
|
$config['cert'] = array();
|
|
|
|
$a_cert = &$config['cert'];
|
|
|
|
$cert_cfg['keylen'] = 2048;
|
|
$cert_cfg['csr_keylen'] = 2048;
|
|
$cert_cfg['digest_alg'] = 'sha256';
|
|
$cert_cfg['type'] = 'server';
|
|
$cert_cfg['lifetime'] = 3650;
|
|
|
|
$dn['countryName'] = 'US';
|
|
$dn['stateOrProvinceName'] = 'TX';
|
|
$dn['localityName'] = 'Austin';
|
|
$dn['organizationName'] = 'Netgate VPN';
|
|
$dn['emailAddress'] = 'email';
|
|
$dn['commonName'] = 'Netgate VPN Server';
|
|
|
|
$cert = array();
|
|
$cert['refid'] = uniqid();
|
|
$cert['descr'] = 'Netgate Auto VPN Server Cert';
|
|
|
|
if (!cert_create($cert, $caref, $cert_cfg['keylen'],
|
|
$cert_cfg['lifetime'], $dn, $cert_cfg['type'], $cert_cfg['digest_alg'])) {
|
|
$ssl_errs = 0;
|
|
while ($ssl_err = openssl_error_string()) {
|
|
$ssl_errs++;
|
|
$last_ssl_err = $ssl_err;
|
|
}
|
|
if ($ssl_errs) {
|
|
echo "Errors creating cert: $last_ssl_err\n";
|
|
return;
|
|
}
|
|
}
|
|
|
|
$a_cert[] = $cert;
|
|
return($cert['refid']);
|
|
}
|
|
|
|
function addOpenVPNServer() {
|
|
global $config;
|
|
|
|
if (!is_array($config['openvpn'])) {
|
|
$config['openvpn'] = array();
|
|
}
|
|
if (!is_array($config['openvpn']['openvpn-server'])) {
|
|
$config['openvpn']['openvpn-server'] = array();
|
|
}
|
|
|
|
$a_server = &$config['openvpn']['openvpn-server'];
|
|
|
|
/* don't do anything if it's previously been done */
|
|
if (isset($a_server[0]['description']) &&
|
|
($a_server[0]['description'] == 'Netgate Auto Remote Access VPN'))
|
|
return;
|
|
|
|
$server['vpnid'] = 0;
|
|
$server['disable'] = '';
|
|
$server['mode'] = 'server_user';
|
|
$server['authmode'] = 'Local Database';
|
|
$server['protocol'] = 'UDP';
|
|
$server['dev_mode'] = 'tun';
|
|
$server['interface'] = 'wan';
|
|
$server['local_port'] = 1194;
|
|
$server['description'] = 'Netgate Auto Remote Access VPN';
|
|
$server['tlsauth_enable'] = 'no';
|
|
$server['autotls_enable'] = 'no';
|
|
$server['caref'] = addCA();
|
|
if (!isset($server['caref']))
|
|
return;
|
|
$server['certref'] = addServerCert($server['caref']);
|
|
if (!isset($server['certref']))
|
|
return;
|
|
$server['dh_length'] = 1024;
|
|
$server['crypto'] = 'AES-128-CBC';
|
|
$server['engine'] = 'none';
|
|
$server['cert_depth'] = 1;
|
|
$server['tunnel_network'] = '172.24.42.0/24';
|
|
$server['gwredir'] = 'yes';
|
|
$server['compression'] = 'yes';
|
|
$server['duplicate_cn'] = true;
|
|
$server['topology_subnet'] = 'yes';
|
|
$server['custom_options'] = 'push "route-ipv6 0::0/1 vpn_gateway";push "route-ipv6 8000::0/1 vpn_gateway";';
|
|
$server['tunnel_networkv6'] = 'fd6f:826b:ed1e::0/64';
|
|
$server['dns_server_enable'] = true;
|
|
$server['dns_server1'] = '172.24.42.1';
|
|
|
|
$a_server[] = $server;
|
|
|
|
openvpn_resync('server', $server);
|
|
return;
|
|
}
|
|
|
|
function configureMgmtNetRules($mgmtnet) {
|
|
global $config;
|
|
|
|
/*
|
|
Since the EC2 VM must be managed over the internet, access to SSH
|
|
& web is open to the outside. By default it is open to anywhere
|
|
because it is unknown at image creation time where the user will be
|
|
coming from. User can pass in a management network to allow in
|
|
the user data field that will be used to replace 'any' in the
|
|
default filter rules.
|
|
|
|
find rules with '_replace_src_with_mgmtnet_' in the description and
|
|
replace the source network with $mgmtnet
|
|
|
|
could also add a tag to look for that indicates the destination
|
|
address (or other attributes) should be substituted
|
|
*/
|
|
|
|
$src_addr_tag = '_replace_src_with_mgmtnet_';
|
|
|
|
if (! (is_ipaddrv4($mgmtnet) || is_subnetv4($mgmtnet)) ) {
|
|
echo "Invalid management subnet/address: $mgmtnet\n";
|
|
return;
|
|
}
|
|
|
|
if (!is_array($config['filter'])) {
|
|
$config['filter'] = array();
|
|
}
|
|
if (!is_array($config['filter']['rule'])) {
|
|
$config['filter']['rule'] = array();
|
|
}
|
|
$a_filter = &$config['filter']['rule'];
|
|
|
|
foreach ($a_filter as &$rule) {
|
|
$pos = strpos($rule['descr'], $src_addr_tag);
|
|
if ($pos !== false) {
|
|
unset($rule['source']['any']);
|
|
$rule['source']['address'] = $mgmtnet;
|
|
$rule['descr'] = str_replace($src_addr_tag, "", $rule['descr']);
|
|
}
|
|
}
|
|
|
|
return(true);
|
|
}
|
|
|
|
function writeOpenVPNConfig($publicIP) {
|
|
global $config, $g;
|
|
|
|
/* check if the first openvpn server is the automatically generated
|
|
remote access VPN server before writing the config */
|
|
if (!is_array($config['openvpn']['openvpn-server']) ||
|
|
!isset($config['openvpn']['openvpn-server'][0]['description']) ||
|
|
($config['openvpn']['openvpn-server'][0]['description'] !=
|
|
'Netgate Auto Remote Access VPN')) {
|
|
return;
|
|
}
|
|
|
|
$cfgDir = "/usr/local/libdata/vpn-profile";
|
|
$ovpnCfgFile = "remote-access-openvpn.ovpn";
|
|
$cfgTemplateDir = "/usr/local/share/{$g['product_name']}-openvpn_connect_profile";
|
|
|
|
if (!file_exists($cfgDir))
|
|
mkdir($cfgDir, 0755, true);
|
|
|
|
/* read the template file and replace the placeholders */
|
|
$newOvpnCfg = file_get_contents("$cfgTemplateDir/$ovpnCfgFile");
|
|
if (!isset($newOvpnCfg))
|
|
return;
|
|
|
|
$newOvpnCfg = str_replace('__PUBLIC_IP__', $publicIP, $newOvpnCfg);
|
|
$ca = $config['ca'][0]['crt'];
|
|
if ($ca) {
|
|
$newOvpnCfg = str_replace('__CA_CRT__', base64_decode($ca), $newOvpnCfg);
|
|
}
|
|
|
|
/* do not write a file if one of the fields was missing */
|
|
if (!($publicIP && $ca))
|
|
return;
|
|
|
|
if (!file_exists("$cfgDir/$ovpnCfgFile") ||
|
|
(file_get_contents("$cfgDir/$ovpnCfgFile") !== $newOvpnCfg))
|
|
file_put_contents("$cfgDir/$ovpnCfgFile", $newOvpnCfg);
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
function initialSystemConfig() {
|
|
global $config, $g;
|
|
|
|
/* admin user should exist already, exit if it doesnt */
|
|
if (!(is_array($config['system']['user']) && isset($config['system']['user'][0]))) {
|
|
echo "Didn't find user data in config. Exiting EC2 setup.\n";
|
|
exit;
|
|
}
|
|
|
|
$a_users = &$config['system']['user'];
|
|
|
|
$ec2_user = 'ec2-user';
|
|
$ec2_id = -1;
|
|
foreach ($a_users as $id => $user) {
|
|
if ($user['name'] == $ec2_user) {
|
|
$ec2_id = $id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
# Create EC2 user when it doesn't exist
|
|
if ($ec2_id == -1) {
|
|
$new_user = array(
|
|
'scope' => 'user',
|
|
'descr' => 'EC2 User',
|
|
'name' => $ec2_user,
|
|
'uid' => $config['system']['nextuid']++
|
|
);
|
|
$ec2_id = count($a_users);
|
|
$a_users[$ec2_id] = $new_user;
|
|
/*
|
|
* Add user to groups so PHP can see the memberships properly
|
|
* or else the user's shell account does not get proper
|
|
* permissions (if applicable) See #5152.
|
|
*/
|
|
local_user_set_groups($new_user, array('admins'));
|
|
local_user_set($new_user);
|
|
/*
|
|
* Add user to groups again to ensure they are set everywhere,
|
|
* otherwise the user may not appear to be a member of the
|
|
* group. See commit:5372d26d9d25d751d16865ed9d46869d3b0ec5e1.
|
|
*/
|
|
local_user_set_groups($new_user, array('admins'));
|
|
write_config("EC2 User created by ec2_setup");
|
|
}
|
|
|
|
$a_ec2_user = &$config['system']['user'][$ec2_id];
|
|
$a_admin_user = &$config['system']['user'][0];
|
|
|
|
/* get the administative SSH Key and add it to the config */
|
|
$ssh_key = retrieveSSHKey();
|
|
if ($ssh_key) {
|
|
echo "SSH Key retrieved: $ssh_key\n";
|
|
} else {
|
|
echo "Failed to retrieve an SSH key for administrative access\n";
|
|
}
|
|
|
|
if ($ssh_key && !isset($a_ec2_user['authorizedkeys'])) {
|
|
$a_ec2_user['authorizedkeys'] = base64_encode($ssh_key);
|
|
}
|
|
|
|
/* get user metadata, set ec2-user password if one was specified */
|
|
$user_data = retrieveUserData();
|
|
if ($user_data && isset($user_data['password']))
|
|
$ec2_user_password = $user_data['password'];
|
|
else
|
|
/* none specified, generate a random one */
|
|
$ec2_user_password = generateRandomPassword();
|
|
|
|
if ($ec2_user_password) {
|
|
$pw_string = "***\n***\n";
|
|
$pw_string .= "*** ec2-user password changed to: $ec2_user_password\n";
|
|
$pw_string .= "***\n***\n";
|
|
local_user_set_password($a_ec2_user, $ec2_user_password);
|
|
local_user_set_password($a_admin_user, $ec2_user_password);
|
|
file_put_contents("/etc/motd-passwd", $pw_string);
|
|
} else {
|
|
@unlink('/etc/motd-passwd');
|
|
echo "No password generated for admin, keeping default password\n";
|
|
}
|
|
local_user_set($a_ec2_user);
|
|
|
|
if ($ssh_key && !isset($a_admin_user['authorizedkeys'])) {
|
|
$a_admin_user['authorizedkeys'] = base64_encode($ssh_key);
|
|
}
|
|
local_user_set($a_admin_user);
|
|
|
|
if ($g['default-config-flavor'] == "ec2" ||
|
|
$g['default-config-flavor'] == "ec2-ic") {
|
|
/* add a disabled remote access OpenVPN server */
|
|
addOpenVPNServer();
|
|
}
|
|
|
|
if (isset($user_data['mgmtnet']))
|
|
configureMgmtNetRules($user_data['mgmtnet']);
|
|
|
|
if (file_exists("/usr/local/pkg/sudo.inc") &&
|
|
is_array($config['installedpackages']['sudo']['config'][0]['row'])) {
|
|
$a_row = &$config['installedpackages']['sudo']['config'][0]['row'];
|
|
|
|
$row_id = -1;
|
|
foreach ($a_row as $id => $row) {
|
|
if ($row['username'] == "user:{$ec2_user}") {
|
|
$row_id = $id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($row_id == -1) {
|
|
$new_row = array(
|
|
'username' => "user:{$ec2_user}",
|
|
'runas' => 'user:root',
|
|
'nopasswd' => 'ON',
|
|
'cmdlist' => 'ALL'
|
|
);
|
|
$a_row[] = $new_row;
|
|
write_config("Sudo configured for EC2 User");
|
|
|
|
require_once("/usr/local/pkg/sudo.inc");
|
|
sudo_write_config();
|
|
}
|
|
}
|
|
|
|
unset($config['system']['doinitialsetup']);
|
|
write_config("EC2 setup completed");
|
|
}
|
|
|
|
switch ($g['default-config-flavor']) {
|
|
case "ec2":
|
|
case "ec2-csm":
|
|
case "ec2-ic":
|
|
case "openstack-csm":
|
|
if (isset($config['system']['doinitialsetup']))
|
|
initialSystemConfig();
|
|
break;
|
|
}
|
|
|
|
switch ($g['default-config-flavor']) {
|
|
case "ec2":
|
|
case "ec2-csm":
|
|
case "ec2-ic":
|
|
$publicIP = retrievePublicIP();
|
|
writeOpenVPNConfig($publicIP);
|
|
break;
|
|
}
|
|
|
|
?>
|