Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@dpneumo
Last active February 18, 2022 08:18
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dpneumo/a71473a38451223a524ad963b06dfb9e to your computer and use it in GitHub Desktop.
Save dpneumo/a71473a38451223a524ad963b06dfb9e to your computer and use it in GitHub Desktop.
Modify cloud-init phone_home module to return a created server's public key for insertion into known_hosts. Works with cloud-iniit version 0.7.9 and later.
# Diff of the original cc_phone_home and the slightly modified version that supports
# including the server pub_keys in the phone_home payload
# Original: https://github.com/number5/cloud-init/blob/master/cloudinit/config/cc_phone_home.py
# A couple of typos in comment lines in the original were elided to make the diff a bit clearer.
# The Centos7 distros I am using do not provide pub_key_dsa.
# I am talking to a Rails app with phone_home.
# A dummy X-CSRF-Token: 1234567890 in the headers simplifies the code on the Rails side.
# I authenticate the phone_home payload by including a token provided in the
# server user-data in the create request.
19d18
< - ``pub_key_dsa``
21a21
> - ``pub_key_ed25519``
35a36
> token: abcd1234
37c38
< - pub_key_dsa
---
> - pub_key_rsa
40a42
> headers: {X-CSRF-Token: 1234567890}
42a45,46
> import json
>
44d47
< from cloudinit import url_helper
52d54
< 'pub_key_dsa',
54a57
> 'pub_key_ed25519',
67a71
> # token: AbCd09876
69a74,75
> # headers: { X-CSRF-Token: 1234567890,
> # Content-Type: application/json }
70a77
>
86a94
> token = ph_cfg.get('token', '')
87a96
> header_dict = ph_cfg.get('headers', {})
107a117
> 'pub_key_ed25519': '/etc/ssh/ssh_host_ed25519_key.pub'
138a149,155
>
> # Assemble post data payload
> payload = real_submit_keys.copy()
> payload.update({ 'token': token })
> serv_data = json.dumps({ 'serv_data': payload })
>
> # Finally
140,142c157,160
< url_helper.read_file_or_url(
< url, data=real_submit_keys, retries=tries, sec_between=3,
< ssl_details=util.fetch_ssl_details(cloud.paths))
---
> util.read_file_or_url(url, data=serv_data,
> retries=tries-1, sec_between=3,
> ssl_details=util.fetch_ssl_details(cloud.paths),
> headers=header_dict)
# This controller receives the phone_home payload acquires the server ip address and writes the known_hosts entry
# The ServerSession class creates and remembers the token sent with the createserver request to DO
# DOServer handles the create server request and the query for the created server's ipaddress
# All requests to the DO api are via https so am making the assumption that responses are valid.
class DoHostkeysController < ApplicationController
protect_from_forgery except: :fingerprint
def fingerprint
raise RuntimeError, "Invalid ServerSession token was returned!" unless valid_server?
File.open(known_hosts,'a') {|f| f << "#{ipaddr} #{hostkey}" }
end
private
def hostkeys_params
permitted = params.require( "serv_data" )
.permit( "token", "instance_id", "pub_key_rsa" )
end
def valid_server?
ServerSession.valid_token?(hostkeys_params['token'])
end
def ipaddr
DoServer.new.get_ipaddr(hostkeys_params['instance_id'])
end
def hostkey
hostkeys_params['pub_key_rsa']
end
def known_hosts
'/home/vagrant/.ssh/known_hosts'
end
end
# Rails routing for the do_hostkeys_controller
Rails.application.routes.draw do
# ---
post 'do_hostkeys/fingerprint', to: 'do_hostkeys#fingerprint', as: 'do_fingerprint'
# ---
end
module UserDataConcern
def user_data
<<~USERDATA
#cloud-config
#{ ph_patch }
#{ phone_home }
USERDATA
end
def ph_patch
ph_path = '/usr/lib/python2.7/site-packages/cloudinit/config/cc_phone_home.py'
<<~PHPATCH
runcmd:
- mv #{ph_path} #{ph_path}.original
write_files:
- path: #{ph_path}
content: |
# Copyright (C) 2011 Canonical Ltd.
# Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
#
# Author: Scott Moser <scott.moser@canonical.com>
# Author: Juerg Haefliger <juerg.haefliger@hp.com>
#
# This file is part of cloud-init. See LICENSE file for license information.
"""
Phone Home
----------
**Summary:** post data to url
This module can be used to post data to a remote host after boot is complete.
If the post url contains the string ``$INSTANCE_ID`` it will be replaced with
the id of the current instance. Either all data can be posted or a list of
keys to post. Available keys are:
- ``pub_key_rsa``
- ``pub_key_ecdsa``
- ``pub_key_ed25519``
- ``instance_id``
- ``hostname``
- ``fdqn``
**Internal name:** ``cc_phone_home``
**Module frequency:** per instance
**Supported distros:** all
**Config keys**::
phone_home:
url: http://example.com/$INSTANCE_ID/
token: abcd1234
post:
- pub_key_rsa
- instance_id
- fqdn
tries: 10
headers: {X-CSRF-Token: 1234567890}
"""
import json
from cloudinit import templater
from cloudinit import util
from cloudinit.settings import PER_INSTANCE
frequency = PER_INSTANCE
POST_LIST_ALL = [
'pub_key_rsa',
'pub_key_ecdsa',
'pub_key_ed25519',
'instance_id',
'hostname',
'fqdn'
]
# phone_home:
# url: http://my.foo.bar/$INSTANCE_ID/
# post: all
# tries: 10
#
# phone_home:
# url: http://my.foo.bar/$INSTANCE_ID/
# token: AbCd09876
# post: [ pub_key_dsa, pub_key_rsa, pub_key_ecdsa, instance_id, hostname,
# fqdn ]
# headers: { X-CSRF-Token: 1234567890,
# Content-Type: application/json }
#
def handle(name, cfg, cloud, log, args):
if len(args) != 0:
ph_cfg = util.read_conf(args[0])
else:
if 'phone_home' not in cfg:
log.debug(("Skipping module named %s, "
"no 'phone_home' configuration found"), name)
return
ph_cfg = cfg['phone_home']
if 'url' not in ph_cfg:
log.warn(("Skipping module named %s, "
"no 'url' found in 'phone_home' configuration"), name)
return
url = ph_cfg['url']
token = ph_cfg.get('token', '')
post_list = ph_cfg.get('post', 'all')
header_dict = ph_cfg.get('headers', {})
tries = ph_cfg.get('tries')
try:
tries = int(tries)
except Exception:
tries = 10
util.logexc(log, "Configuration entry 'tries' is not an integer, "
"using %s instead", tries)
if post_list == "all":
post_list = POST_LIST_ALL
all_keys = {}
all_keys['instance_id'] = cloud.get_instance_id()
all_keys['hostname'] = cloud.get_hostname()
all_keys['fqdn'] = cloud.get_hostname(fqdn=True)
pubkeys = {
'pub_key_dsa': '/etc/ssh/ssh_host_dsa_key.pub',
'pub_key_rsa': '/etc/ssh/ssh_host_rsa_key.pub',
'pub_key_ecdsa': '/etc/ssh/ssh_host_ecdsa_key.pub',
'pub_key_ed25519': '/etc/ssh/ssh_host_ed25519_key.pub'
}
for (name, path) in pubkeys.items():
try:
all_keys[name] = util.load_file(path)
except Exception:
util.logexc(log, "%s: failed to open, can not phone home that "
"data!", path)
submit_keys = {}
for k in post_list:
if k in all_keys:
submit_keys[k] = all_keys[k]
else:
submit_keys[k] = None
log.warn(("Requested key %s from 'post'"
" configuration list not available"), k)
# Get them ready to be posted
real_submit_keys = {}
for (k, v) in submit_keys.items():
if v is None:
real_submit_keys[k] = 'N/A'
else:
real_submit_keys[k] = str(v)
# In case the url is parameterized
url_params = {
'INSTANCE_ID': all_keys['instance_id'],
}
url = templater.render_string(url, url_params)
# Assemble post data payload
payload = real_submit_keys.copy()
payload.update({ 'token': token })
serv_data = json.dumps({ 'serv_data': payload })
# Finally
try:
util.read_file_or_url(url, data=serv_data,
retries=tries-1, sec_between=3,
ssl_details=util.fetch_ssl_details(cloud.paths),
headers=header_dict)
except Exception:
util.logexc(log, "Failed to post phone home data to %s in %s tries",
url, tries)
# vi: ts=4 expandtab
PHPATCH
end
def phone_home
<<~PHONEHOME
phone_home:
url: "http://123.222.222.222:3000/do_hostkeys/fingerprint"
token: #{ServerSession.server_session_token}
headers:
X-CSRF-Token: #{ServerSession.csrf_token}
Content-Type: application/json
post: [ pub_key_rsa, instance_id ]
tries: 2
PHONEHOME
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment