├── cloud-init.pl └── README.md /cloud-init.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | # Copyright (c) 2015 Pierre-Yves Ritschard 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | use CPAN::Meta::YAML; 25 | use HTTP::Tiny; 26 | use File::Basename; 27 | use File::Path qw(make_path mkpath); 28 | use File::Temp qw(tempfile); 29 | use IO::Uncompress::Gunzip qw(gunzip $GunzipError) ; 30 | 31 | use strict; 32 | 33 | use constant { 34 | METADATA_HOST => "169.254.169.254", 35 | }; 36 | 37 | sub get_data { 38 | my ($host, $path) = @_; 39 | my $response = HTTP::Tiny->new->get("http://$host/latest/$path"); 40 | return unless $response->{success}; 41 | return $response->{content}; 42 | } 43 | 44 | sub get_default_fqdn { 45 | my $host = METADATA_HOST; 46 | 47 | my $local_hostname = get_data($host, 'meta-data/local-hostname'); 48 | return $local_hostname . '.my.domain'; 49 | } 50 | 51 | sub set_hostname { 52 | my $fqdn = shift; 53 | 54 | open my $fh, ">", "/etc/myname"; 55 | printf $fh "%s\n", $fqdn; 56 | close $fh; 57 | system("hostname " . $fqdn); 58 | } 59 | 60 | sub add_etc_hosts_entry { 61 | my $fqdn = shift; 62 | my ($shortname) = split(/\./, $fqdn); 63 | 64 | open my $fh, ">>", "/etc/hosts"; 65 | printf $fh "127.0.1.1 %s %s\n", $shortname, $fqdn; 66 | close $fh; 67 | } 68 | 69 | sub install_pubkeys { 70 | my $pubkeys = shift; 71 | 72 | make_path('/root/.ssh', { verbose => 0, mode => 0700 }); 73 | open my $fh, ">>", "/root/.ssh/authorized_keys"; 74 | printf $fh "#-- key added by cloud-init at your request --#\n"; 75 | printf $fh "%s\n", $pubkeys; 76 | close $fh; 77 | } 78 | 79 | sub apply_user_data { 80 | my $data = shift; 81 | 82 | my $fqdn = $data->{fqdn} // get_default_fqdn; 83 | 84 | set_hostname($fqdn); 85 | 86 | if (!defined($data->{manage_etc_hosts}) || 87 | $data->{manage_etc_hosts} eq 'true' || 88 | $data->{manage_etc_hosts} eq 'localhost') { 89 | add_etc_hosts_entry($fqdn); 90 | } 91 | 92 | if (defined($data->{ssh_authorized_keys})) { 93 | install_pubkeys join("\n", @{ $data->{ssh_authorized_keys} }); 94 | } 95 | 96 | if (defined($data->{packages})) { 97 | foreach my $package (@{ $data->{packages} }) { 98 | system("pkg_add " . $package); 99 | } 100 | } 101 | 102 | if (defined($data->{write_files})) { 103 | foreach my $item (@{ $data->{write_files} }) { 104 | mkpath [dirname($item->{path})], 0, 0755; 105 | open my $fh, ">", $item->{path}; 106 | print $fh $item->{content}; 107 | if (defined($item->{permissions})) { 108 | my $perms = oct($item->{permissions}); 109 | chmod($perms, $fh); 110 | } 111 | if (defined($item->{owner})) { 112 | my ($user_name, $group_name) = split(/\:/, $item->{owner}); 113 | my $uid = getpwnam $user_name; 114 | my $gid = getgrnam $group_name; 115 | chown $uid, $gid, $fh; 116 | } 117 | close $fh; 118 | } 119 | } 120 | 121 | if (defined($data->{runcmd})) { 122 | foreach my $runcmd (@{ $data->{runcmd} }) { 123 | system("sh -c \"$runcmd\""); 124 | } 125 | } 126 | } 127 | 128 | sub run_user_script { 129 | my $data = shift; 130 | 131 | my ($fh, $filename) = tempfile("/tmp/cloud-config-XXXXXX"); 132 | print $fh $data; 133 | chmod(0700, $fh); 134 | close $fh; 135 | system("sh -c \"$filename && rm $filename\""); 136 | } 137 | 138 | sub cloud_init { 139 | my $host = METADATA_HOST; 140 | 141 | my $compressed = get_data($host, 'user-data'); 142 | my $data; 143 | gunzip \$compressed => \$data; 144 | 145 | my $pubkeys = get_data($host, 'meta-data/public-keys'); 146 | chomp($pubkeys); 147 | install_pubkeys $pubkeys; 148 | 149 | if ($data =~ /^#cloud-config/) { 150 | $data = CPAN::Meta::YAML->read_string($data)->[0]; 151 | apply_user_data($data); 152 | } else { 153 | set_hostname(get_default_fqdn); 154 | add_etc_hosts_entry(get_default_fqdn); 155 | 156 | if ($data =~ /^#\!/) { 157 | run_user_script($data); 158 | } 159 | } 160 | } 161 | 162 | sub action_deploy { 163 | #-- rc.firsttime stub 164 | open my $fh, ">>", "/etc/rc.firsttime"; 165 | print $fh <<'EOF'; 166 | # run cloud-init 167 | path=/usr/local/libdata/cloud-init.pl 168 | echo -n "exoscale first boot: " 169 | perl $path cloud-init && echo "done." 170 | EOF 171 | close $fh; 172 | 173 | #-- remove generated keys and seeds 174 | unlink glob "/etc/ssh/ssh_host*"; 175 | unlink "/etc/random.seed"; 176 | unlink "/var/db/host.random"; 177 | unlink "/etc/isakmpd/private/local.key"; 178 | unlink "/etc/isakmpd/local.pub"; 179 | unlink "/etc/iked/private/local.key"; 180 | unlink "/etc/isakmpd/local.pub"; 181 | 182 | #-- remove cruft 183 | unlink "/tmp/*"; 184 | unlink "/var/db/dhclient.leases.vio0"; 185 | 186 | #-- disable root password 187 | system("chpass -a 'root:*:0:0:daemon:0:0:Charlie &:/root:/bin/ksh'") 188 | } 189 | 190 | #-- main 191 | my ($action) = @ARGV; 192 | 193 | action_deploy if ($action eq 'deploy'); 194 | cloud_init if ($action eq 'cloud-init'); 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OpenBSD initialization for cloud environments 2 | ============================================= 3 | 4 | `openbsd-cloud-init` provides a dependency-free solution 5 | for initializing [OpenBSD](http://www.openbsd.org) instances within cloud environments. 6 | 7 | The aim is to provide loose compatibility with 8 | [cloud-init](https://cloudinit.readthedocs.org/en/latest/) which has 9 | positioned itself as the standard solution to perform first-boot 10 | changes. 11 | 12 | ## Scope of openbsd-cloud-init 13 | 14 | To keep within the spirit of security promoted by [OpenBSD](http://www.openbsd.org), 15 | this tool will limit itself to a single first-boot run and will be as unintrusive 16 | as possible by default. The following actions are currently supported: 17 | 18 | - SSH [authorized_keys](http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man8/sshd.8?query=sshd&sec=8) personalization if requested. 19 | - Persistent hostname personalization if requested. 20 | - Local host resolution personalization unless requested otherwise. 21 | - Optional custom script execution. 22 | - Packages installation (pkg_add) support. 23 | - Custom commands (runcmd) execution. 24 | 25 | ## Future improvements 26 | 27 | - [ ] Root disk resize 28 | - [ ] Cloud-init user and group creation support 29 | - [ ] Cloud-init write-file support 30 | - [ ] Cloud-init custom package install support 31 | - [ ] Cloud-init puppet initialization support 32 | - [ ] Cloud-init resolv.conf personalization support 33 | 34 | ## Caveats 35 | 36 | As it stands, `openbsd-cloud-init` will only work in KVM + virtio environments when metadata is 37 | served from the same IP. 38 | 39 | ## Installing OpenBSD with openbsd-cloud-init support 40 | 41 | As far as installing `openbsd-cloud-init` is concerned, a standard installation should 42 | be carried out. Before the final reboot, carry out the following actions: 43 | 44 | ```bash 45 | # mount /dev/sd0a /mnt 46 | # mount /dev/sd0X /mnt/usr 47 | # /mnt/usr/sbin/chroot /mnt 48 | # mount -a 49 | # ftp -o /usr/local/libdata/cloud-init.pl http:////cloud-init.pl 50 | # perl /usr/local/libdata/cloud-init.pl deploy 51 | ``` 52 | 53 | The last deploy step will carry out the following actions: 54 | 55 | - Remove the configured root password, effectively disabling password logins 56 | - Remove generated keys (for ike, isakmpd and SSH) and random seeds. 57 | - Configure openbsd-cloud-init to run in `/etc/rc.local` 58 | - Add a first boot indication by touch `/etc/cloud.init` 59 | 60 | ## Example environment 61 | 62 | To create a compatible environment, the following steps can be taken, 63 | assuming a Linux + KVM host environment: 64 | 65 | Setting up a bridge for tap networking: 66 | 67 | ```bash 68 | # brctl addbr br0 69 | # ip link set br0 up 70 | # ip addr add 10.0.38.1/24 dev br0 71 | ``` 72 | 73 | Configure dnsmasq to serve on the bridge: 74 | 75 | ``` 76 | interface=br0 77 | bind-interfaces 78 | dhcp-range=10.0.38.50,10.0.38.100,12h 79 | domain=spootnik.org 80 | ``` 81 | 82 | Serve mock metadata: 83 | 84 | Using `python -m http.server 80` (as root) you can serve the following 85 | directory structure: 86 | 87 | ``` 88 | ./cloud-init.pl => this script 89 | ./latest/meta-data/public-keys => "ssh-rsa ..." (your pubkey) 90 | ./latest/user-data => "#cloud-config\nfqdn: some.host.name\nmanage_etc_hosts: true\n" 91 | ``` 92 | 93 | Create a suitable disk (for instance `qemu-img -f qcow2 basedisk.qcow2 10G`), then 94 | start an instance with an OpenBSD iso: 95 | 96 | 97 | 98 | ``` 99 | qemu-system-x86_64 \ 100 | -M pc-1.0 -enable-kvm -nodefconfig -nodefaults \ 101 | -rtc base=utc -cpu host -smp cpus=4 -m 2048 -vga cirrus \ 102 | -netdev tap,id=hostnet0,vhost=on,ifname=tap0,script=qemu-ifup \ 103 | -device virtio-net-pci,netdev=hostnet0,id=net0,mac=06:f8:ee:00:00:cf,bus=pci.0,addr=0x3 \ 104 | -drive file=basedisk.qcow2,format=qcow2,cache=none,if=none,id=drive-virtio-disk0 \ 105 | -device virtio-blk-pci,bus=pci.0,addr=0x4,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=2 \ 106 | -device isa-serial,chardev=charserial0,id=serial0 \ 107 | -chardev pty,id=charserial0 \ 108 | -name openbsd-guest -uuid 9e182286-92ec-4655-8b91-a1969fc0cbbb \ 109 | -cdrom install56.iso -boot d 110 | ``` 111 | 112 | Install as explained above, then copy the resulting image, you have a template! 113 | It can now be started with: 114 | 115 | ``` 116 | qemu-system-x86_64 \ 117 | -M pc-1.0 -enable-kvm -nodefconfig -nodefaults \ 118 | -rtc base=utc -cpu host -smp cpus=4 -m 2048 -vga cirrus \ 119 | -netdev tap,id=hostnet0,vhost=on,ifname=tap0,script=qemu-ifup \ 120 | -device virtio-net-pci,netdev=hostnet0,id=net0,mac=06:f8:ee:00:00:cf,bus=pci.0,addr=0x3 \ 121 | -drive file=basedisk.qcow2,format=qcow2,cache=none,if=none,id=drive-virtio-disk0 \ 122 | -device virtio-blk-pci,bus=pci.0,addr=0x4,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=2 \ 123 | -device isa-serial,chardev=charserial0,id=serial0 \ 124 | -chardev pty,id=charserial0 \ 125 | -name openbsd-guest -uuid 9e182286-92ec-4655-8b91-a1969fc0cbbb 126 | ``` 127 | 128 | And will fetch personalization from your mock metadata server, giving you 129 | SSH public key access to a machine with a correct hostname and hosts file. 130 | 131 | ## License 132 | 133 | ``` 134 | Copyright (c) 2015 Pierre-Yves Ritschard 135 | 136 | Permission is hereby granted, free of charge, to any person obtaining 137 | a copy of this software and associated documentation files (the 138 | "Software"), to deal in the Software without restriction, including 139 | without limitation the rights to use, copy, modify, merge, publish, 140 | distribute, sublicense, and/or sell copies of the Software, and to 141 | permit persons to whom the Software is furnished to do so, subject to 142 | the following conditions: 143 | 144 | The above copyright notice and this permission notice shall be 145 | included in all copies or substantial portions of the Software. 146 | 147 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 148 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 149 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 150 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 151 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 152 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 153 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 154 | ``` 155 | --------------------------------------------------------------------------------