Project

General

Profile

Bug #14772 ยป ec2_setup.php

Suggested update - Cameron Epp, 09/11/2023 06:50 PM

 
1
#!/usr/local/bin/php -f
2
<?php
3
/*
4
 * ec2_setup.php
5
 *
6
 * part of pfSense (https://www.pfsense.org)
7
 * Copyright (c) 2016-2023 Rubicon Communications, LLC (Netgate)
8
 * All rights reserved.
9
 *
10
 * Licensed under the Apache License, Version 2.0 (the "License");
11
 * you may not use this file except in compliance with the License.
12
 * You may obtain a copy of the License at
13
 *
14
 * http://www.apache.org/licenses/LICENSE-2.0
15
 *
16
 * Unless required by applicable law or agreed to in writing, software
17
 * distributed under the License is distributed on an "AS IS" BASIS,
18
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
 * See the License for the specific language governing permissions and
20
 * limitations under the License.
21
 */
22

    
23
require_once("globals.inc");
24
require_once("config.inc");
25
require_once("auth.inc");
26
require_once("interfaces.inc");
27
require_once("certs.inc");
28
require_once("openvpn.inc");
29

    
30
function retrieveMetaData($url) {
31
	if (!$url)
32
		return;
33
        
34
	$curl = curl_init();
35

    
36
    /* first get the instance token which we will use to 
37
        authenticate the subsequent call */
38
    
39
    $token_url = "http://169.254.169.254/latest/api/token";
40
    $headers = array (
41
        'X-aws-ec2-metadata-token-ttl-seconds: 10' );
42
    curl_setopt($curl, CURLOPT_URL, $token_url);
43
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers );
44
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true );
45
    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PUT" );
46
	curl_setopt($curl, CURLOPT_FAILONERROR, true);
47
	curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 15);
48
	curl_setopt($curl, CURLOPT_TIMEOUT, 30);
49
	$token = curl_exec($curl);
50

    
51
    /* now build the 'real' request and send it along with the
52
       token for authentication */
53

    
54
    $headers = array (
55
        'X-aws-ec2-metadata-token: '.$token );
56
    curl_setopt($curl, CURLOPT_URL, $url);
57
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers );
58
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true );
59
    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "GET" );
60
	curl_setopt($curl, CURLOPT_FAILONERROR, true);
61
	curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 15);
62
	curl_setopt($curl, CURLOPT_TIMEOUT, 30);
63
	$metadata = curl_exec($curl);
64
	curl_close($curl);
65

    
66
	return($metadata);
67
}
68

    
69

    
70
function retrieveSSHKey() {
71
	global $g;
72

    
73
	if ($g['default-config-flavor'] == "openstack-csm") {
74
		$url = "http://169.254.169.254/latest/meta-data/public-ipv4";
75
	} else {
76
		$url = "http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key";
77
	}
78
	return(retrieveMetaData($url));
79
}
80

    
81
function retrieveUserData() {
82
	$url = "http://169.254.169.254/latest/user-data/";
83
	$user_data = retrieveMetaData($url);
84

    
85
	if (!$user_data)
86
		return;
87

    
88
	/* userdata is formatted like this:
89
	   foo1=bar1:foo2=bar2:...:fooN=barN
90
	   what, were you raised in a barN? */
91

    
92
	$kv_pairs = explode(':', $user_data);
93
	foreach ($kv_pairs as $pair) {
94
		list($key, $value) = explode("=", $pair, 2);
95
		$ud[$key] = $value;
96
	}
97

    
98
	return($ud);
99
}
100

    
101
function retrievePublicIP() {
102
	$wanintf = get_real_wan_interface();
103
	$macaddr = get_interface_mac($wanintf);
104
	if (!$macaddr)
105
		return;
106

    
107
	$url = "http://169.254.169.254/latest/meta-data/network/interfaces/macs/$macaddr/public-ipv4s";
108
	$public_ipv4 = retrieveMetaData($url);
109

    
110
	if (is_ipaddrv4($public_ipv4)) {
111
		$natipfile = "/var/db/natted_{$wanintf}_ip";
112
		file_put_contents($natipfile, $public_ipv4);
113
		return($public_ipv4);
114
	}
115

    
116
	return;
117
}
118

    
119
function generateRandomPassword($length = 15) {
120
	/* get some random bytes. use them as offsets into the space of
121
           printable ascii characters. 32-126 is the printable characters.
122
	   Omit 32 itself since it might be confusing if there is a space
123
	   in the password.
124
	*/
125

    
126
	$range_size = 126 - 33 + 1;
127
	$random_bytes = str_split(openssl_random_pseudo_bytes($length));
128

    
129
	for ($i = 0; $i < $length; $i++) {
130

    
131
		$offset = ord($random_bytes[$i]) % $range_size;
132
		$password .= chr(33 + $offset);
133

    
134
	}
135

    
136
	return $password;
137
}
138

    
139
function addCA() {
140
	global $config;
141
	if (!is_array($config['ca']))
142
		$config['ca'] = array();
143

    
144
	$a_ca = &$config['ca'];
145

    
146
	$ca_cfg['keylen']     = 2048;
147
	$ca_cfg['digest_alg'] = 'sha256';
148
	$ca_cfg['lifetime']   = 3650;
149

    
150
	$dn['countryName']         = 'US';
151
	$dn['stateOrProvinceName'] = 'TX';
152
	$dn['localityName']        = 'Austin';
153
	$dn['organizationName']    = 'Netgate VPN';
154
	$dn['emailAddress']        = 'email';
155
	$dn['commonName']          = 'Netgate VPN CA';
156

    
157
	$ca = array();
158
	$ca['refid'] = uniqid();
159
	$ca['descr'] = 'Netgate Auto VPN CA';
160

    
161
	if (!ca_create($ca, $ca_cfg['keylen'], $ca_cfg['lifetime'], $dn, $ca_cfg['digest_alg'])) {
162
		$ssl_errs = 0;
163
		while ($ssl_err = openssl_error_string()) {
164
			$ssl_errs++;
165
			$last_ssl_err = $ssl_err;
166
		}
167
		if ($ssl_errs) {
168
			echo "Errors creating CA cert: $last_ssl_err\n";
169
			return;
170
		}
171
	}
172

    
173
	$a_ca[] = $ca;
174
	return($ca['refid']);
175
}
176

    
177
function addServerCert($caref) {
178
	global $config;
179

    
180
	if (!is_array($config['cert']))
181
	$config['cert'] = array();
182

    
183
	$a_cert = &$config['cert'];
184

    
185
	$cert_cfg['keylen'] = 2048;
186
	$cert_cfg['csr_keylen'] = 2048;
187
	$cert_cfg['digest_alg'] = 'sha256';
188
	$cert_cfg['type'] = 'server';
189
	$cert_cfg['lifetime'] = 3650;
190

    
191
	$dn['countryName']         = 'US';
192
	$dn['stateOrProvinceName'] = 'TX';
193
	$dn['localityName']        = 'Austin';
194
	$dn['organizationName']    = 'Netgate VPN';
195
	$dn['emailAddress']        = 'email';
196
	$dn['commonName']          = 'Netgate VPN Server';
197

    
198
	$cert = array();
199
	$cert['refid'] = uniqid();
200
	$cert['descr'] = 'Netgate Auto VPN Server Cert';
201

    
202
	if (!cert_create($cert, $caref, $cert_cfg['keylen'],
203
	    $cert_cfg['lifetime'], $dn, $cert_cfg['type'], $cert_cfg['digest_alg'])) {
204
		$ssl_errs = 0;
205
		while ($ssl_err = openssl_error_string()) {
206
			$ssl_errs++;
207
			$last_ssl_err = $ssl_err;
208
		}
209
		if ($ssl_errs) {
210
			echo "Errors creating cert: $last_ssl_err\n";
211
			return;
212
		}
213
	}
214

    
215
	$a_cert[] = $cert;
216
	return($cert['refid']);
217
}
218

    
219
function addOpenVPNServer() {
220
	global $config;
221

    
222
	if (!is_array($config['openvpn'])) {
223
		$config['openvpn'] = array();
224
	}
225
	if (!is_array($config['openvpn']['openvpn-server'])) {
226
		$config['openvpn']['openvpn-server'] = array();
227
	}
228

    
229
	$a_server = &$config['openvpn']['openvpn-server'];
230

    
231
	/* don't do anything if it's previously been done */
232
	if (isset($a_server[0]['description']) &&
233
	    ($a_server[0]['description'] == 'Netgate Auto Remote Access VPN'))
234
		return;
235

    
236
	$server['vpnid'] = 0;
237
	$server['disable'] = '';
238
	$server['mode'] = 'server_user';
239
	$server['authmode'] = 'Local Database';
240
	$server['protocol'] = 'UDP';
241
	$server['dev_mode'] = 'tun';
242
	$server['interface'] = 'wan';
243
	$server['local_port'] = 1194;
244
	$server['description'] = 'Netgate Auto Remote Access VPN';
245
	$server['tlsauth_enable'] = 'no';
246
	$server['autotls_enable'] = 'no';
247
	$server['caref'] = addCA();
248
	if (!isset($server['caref']))
249
		return;
250
	$server['certref'] = addServerCert($server['caref']);
251
	if (!isset($server['certref']))
252
		return;
253
	$server['dh_length'] = 1024;
254
	$server['crypto'] = 'AES-128-CBC';
255
	$server['engine'] = 'none';
256
	$server['cert_depth'] = 1;
257
	$server['tunnel_network'] = '172.24.42.0/24';
258
	$server['gwredir'] = 'yes';
259
	$server['compression']  = 'yes';
260
	$server['duplicate_cn'] = true;
261
	$server['topology_subnet'] = 'yes';
262
	$server['custom_options'] = 'push "route-ipv6 0::0/1 vpn_gateway";push "route-ipv6 8000::0/1 vpn_gateway";';
263
	$server['tunnel_networkv6'] = 'fd6f:826b:ed1e::0/64';
264
	$server['dns_server_enable'] = true;
265
	$server['dns_server1'] = '172.24.42.1';
266

    
267
	$a_server[] = $server;
268

    
269
	openvpn_resync('server', $server);
270
	return;
271
}
272

    
273
function configureMgmtNetRules($mgmtnet) {
274
	global $config;
275

    
276
	/*
277
	   Since the EC2 VM must be managed over the internet, access to SSH
278
	   & web is open to the outside. By default it is open to anywhere
279
	   because it is unknown at image creation time where the user will be
280
	   coming from. User can pass in a management network to allow in
281
	   the user data field that will be used to replace 'any' in the
282
	   default filter rules.
283

    
284
	   find rules with '_replace_src_with_mgmtnet_' in the description and
285
	   replace the source network with $mgmtnet
286

    
287
	   could also add a tag to look for that indicates the destination
288
	   address (or other attributes) should be substituted
289
	*/
290

    
291
	$src_addr_tag = '_replace_src_with_mgmtnet_';
292

    
293
	if (! (is_ipaddrv4($mgmtnet) || is_subnetv4($mgmtnet)) ) {
294
		echo "Invalid management subnet/address: $mgmtnet\n";
295
		return;
296
	}
297

    
298
	if (!is_array($config['filter'])) {
299
		$config['filter'] = array();
300
	}
301
	if (!is_array($config['filter']['rule'])) {
302
		$config['filter']['rule'] = array();
303
	}
304
	$a_filter = &$config['filter']['rule'];
305

    
306
	foreach ($a_filter as &$rule) {
307
		$pos = strpos($rule['descr'], $src_addr_tag);
308
		if ($pos !== false) {
309
			unset($rule['source']['any']);
310
			$rule['source']['address'] = $mgmtnet;
311
			$rule['descr'] = str_replace($src_addr_tag, "", $rule['descr']);
312
		}
313
	}
314

    
315
	return(true);
316
}
317

    
318
function writeOpenVPNConfig($publicIP) {
319
	global $config, $g;
320

    
321
	/* check if the first openvpn server is the automatically generated
322
	   remote access VPN server before writing the config */
323
	if (!is_array($config['openvpn']['openvpn-server']) ||
324
	    !isset($config['openvpn']['openvpn-server'][0]['description']) ||
325
	    ($config['openvpn']['openvpn-server'][0]['description'] !=
326
	     'Netgate Auto Remote Access VPN')) {
327
		return;
328
	}
329

    
330
	$cfgDir             = "/usr/local/libdata/vpn-profile";
331
	$ovpnCfgFile        = "remote-access-openvpn.ovpn";
332
	$cfgTemplateDir     = "/usr/local/share/{$g['product_name']}-openvpn_connect_profile";
333

    
334
	if (!file_exists($cfgDir))
335
		mkdir($cfgDir, 0755, true);
336

    
337
	/* read the template file and replace the placeholders */
338
	$newOvpnCfg = file_get_contents("$cfgTemplateDir/$ovpnCfgFile");
339
	if (!isset($newOvpnCfg))
340
		return;
341

    
342
	$newOvpnCfg = str_replace('__PUBLIC_IP__', $publicIP, $newOvpnCfg);
343
	$ca = $config['ca'][0]['crt'];
344
	if ($ca) {
345
		$newOvpnCfg = str_replace('__CA_CRT__', base64_decode($ca), $newOvpnCfg);
346
	}
347

    
348
	/* do not write a file if one of the fields was missing */
349
	if (!($publicIP && $ca))
350
		return;
351

    
352
	if (!file_exists("$cfgDir/$ovpnCfgFile") ||
353
	    (file_get_contents("$cfgDir/$ovpnCfgFile") !== $newOvpnCfg))
354
		file_put_contents("$cfgDir/$ovpnCfgFile", $newOvpnCfg);
355

    
356
	return;
357
}
358

    
359

    
360
function initialSystemConfig() {
361
	global $config, $g;
362

    
363
	/* admin user should exist already, exit if it doesnt */
364
	if (!(is_array($config['system']['user']) && isset($config['system']['user'][0]))) {
365
		echo "Didn't find user data in config. Exiting EC2 setup.\n";
366
		exit;
367
	}
368

    
369
	$a_users = &$config['system']['user'];
370

    
371
	$ec2_user = 'ec2-user';
372
	$ec2_id = -1;
373
	foreach ($a_users as $id => $user) {
374
		if ($user['name'] == $ec2_user) {
375
			$ec2_id = $id;
376
			break;
377
		}
378
	}
379

    
380
	# Create EC2 user when it doesn't exist
381
	if ($ec2_id == -1) {
382
		$new_user = array(
383
			'scope' => 'user',
384
			'descr' => 'EC2 User',
385
			'name' => $ec2_user,
386
			'uid' => $config['system']['nextuid']++
387
		);
388
		$ec2_id = count($a_users);
389
		$a_users[$ec2_id] = $new_user;
390
		/*
391
		 * Add user to groups so PHP can see the memberships properly
392
		 * or else the user's shell account does not get proper
393
		 * permissions (if applicable) See #5152.
394
		 */
395
		local_user_set_groups($new_user, array('admins'));
396
		local_user_set($new_user);
397
		/*
398
		 * Add user to groups again to ensure they are set everywhere,
399
		 * otherwise the user may not appear to be a member of the
400
		 * group. See commit:5372d26d9d25d751d16865ed9d46869d3b0ec5e1.
401
		 */
402
		local_user_set_groups($new_user, array('admins'));
403
		write_config("EC2 User created by ec2_setup");
404
	}
405

    
406
	$a_ec2_user = &$config['system']['user'][$ec2_id];
407
	$a_admin_user = &$config['system']['user'][0];
408

    
409
	/* get the administative SSH Key and add it to the config */
410
	$ssh_key = retrieveSSHKey();
411
	if ($ssh_key) {
412
		echo "SSH Key retrieved: $ssh_key\n";
413
	} else {
414
		echo "Failed to retrieve an SSH key for administrative access\n";
415
	}
416

    
417
	if ($ssh_key && !isset($a_ec2_user['authorizedkeys'])) {
418
		$a_ec2_user['authorizedkeys'] = base64_encode($ssh_key);
419
	}
420

    
421
	/* get user metadata, set ec2-user password if one was specified */
422
	$user_data = retrieveUserData();
423
	if ($user_data && isset($user_data['password']))
424
		$ec2_user_password = $user_data['password'];
425
	else
426
		/* none specified, generate a random one */
427
		$ec2_user_password = generateRandomPassword();
428

    
429
	if ($ec2_user_password) {
430
		$pw_string = "***\n***\n";
431
		$pw_string .= "*** ec2-user password changed to: $ec2_user_password\n";
432
		$pw_string .= "***\n***\n";
433
		local_user_set_password($a_ec2_user, $ec2_user_password);
434
		local_user_set_password($a_admin_user, $ec2_user_password);
435
		file_put_contents("/etc/motd-passwd", $pw_string);
436
	} else {
437
		@unlink('/etc/motd-passwd');
438
		echo "No password generated for admin, keeping default password\n";
439
	}
440
	local_user_set($a_ec2_user);
441

    
442
	if ($ssh_key && !isset($a_admin_user['authorizedkeys'])) {
443
		$a_admin_user['authorizedkeys'] = base64_encode($ssh_key);
444
	}
445
	local_user_set($a_admin_user);
446

    
447
	if ($g['default-config-flavor'] == "ec2" ||
448
	    $g['default-config-flavor'] == "ec2-ic") {
449
		/* add a disabled remote access OpenVPN server */
450
		addOpenVPNServer();
451
	}
452

    
453
	if (isset($user_data['mgmtnet']))
454
		configureMgmtNetRules($user_data['mgmtnet']);
455

    
456
	if (file_exists("/usr/local/pkg/sudo.inc") &&
457
	    is_array($config['installedpackages']['sudo']['config'][0]['row'])) {
458
		$a_row = &$config['installedpackages']['sudo']['config'][0]['row'];
459

    
460
		$row_id = -1;
461
		foreach ($a_row as $id => $row) {
462
			if ($row['username'] == "user:{$ec2_user}") {
463
				$row_id = $id;
464
				break;
465
			}
466
		}
467

    
468
		if ($row_id == -1) {
469
			$new_row = array(
470
				'username' => "user:{$ec2_user}",
471
				'runas' => 'user:root',
472
				'nopasswd' => 'ON',
473
				'cmdlist' => 'ALL'
474
			);
475
			$a_row[] = $new_row;
476
			write_config("Sudo configured for EC2 User");
477

    
478
			require_once("/usr/local/pkg/sudo.inc");
479
			sudo_write_config();
480
		}
481
	}
482

    
483
	unset($config['system']['doinitialsetup']);
484
	write_config("EC2 setup completed");
485
}
486

    
487
switch ($g['default-config-flavor']) {
488
case "ec2":
489
case "ec2-csm":
490
case "ec2-ic":
491
case "openstack-csm":
492
	if (isset($config['system']['doinitialsetup']))
493
		initialSystemConfig();
494
	break;
495
}
496

    
497
switch ($g['default-config-flavor']) {
498
case "ec2":
499
case "ec2-csm":
500
case "ec2-ic":
501
	$publicIP = retrievePublicIP();
502
	writeOpenVPNConfig($publicIP);
503
	break;
504
}
505

    
506
?>
    (1-1/1)