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
|
?>
|