github.com/greenboxal/deis@v1.12.1/contrib/linode/apply-firewall.py (about) 1 #!/usr/bin/env python 2 """ 3 Apply a "Security Group" to the members of an etcd cluster. 4 5 Usage: apply-firewall.py 6 """ 7 import os 8 import re 9 import string 10 import argparse 11 from threading import Thread 12 import uuid 13 14 import colorama 15 from colorama import Fore, Style 16 import paramiko 17 import requests 18 import sys 19 import yaml 20 21 22 def get_nodes_from_args(args): 23 if args.discovery_url is not None: 24 return get_nodes_from_discovery_url(args.discovery_url) 25 26 return get_nodes_from_discovery_url(get_discovery_url_from_user_data()) 27 28 29 def get_nodes_from_discovery_url(discovery_url): 30 try: 31 nodes = [] 32 json = requests.get(discovery_url).json() 33 discovery_nodes = json['node']['nodes'] 34 for node in discovery_nodes: 35 value = node['value'] 36 ip = re.search('([0-9]{1,3}\.){3}[0-9]{1,3}', value).group(0) 37 nodes.append(ip) 38 return nodes 39 except: 40 raise IOError('Could not load nodes from discovery url ' + discovery_url) 41 42 43 def get_discovery_url_from_user_data(): 44 name = 'linode-user-data.yaml' 45 log_info('Loading discovery url from ' + name) 46 try: 47 current_dir = os.path.dirname(__file__) 48 user_data_file = file(os.path.abspath(os.path.join(current_dir, name)), 'r') 49 user_data_yaml = yaml.safe_load(user_data_file) 50 return user_data_yaml['coreos']['etcd2']['discovery'] 51 except: 52 raise IOError('Could not load discovery url from ' + name) 53 54 55 def validate_ip_address(ip): 56 return True if re.match('([0-9]{1,3}\.){3}[0-9]{1,3}', ip) else False 57 58 59 def get_firewall_contents(node_ips, private=False): 60 rules_template_text = """*filter 61 :INPUT DROP [0:0] 62 :FORWARD DROP [0:0] 63 :OUTPUT ACCEPT [0:0] 64 :DOCKER - [0:0] 65 :Firewall-INPUT - [0:0] 66 -A INPUT -j Firewall-INPUT 67 -A FORWARD -j Firewall-INPUT 68 -A Firewall-INPUT -i lo -j ACCEPT 69 -A Firewall-INPUT -p icmp --icmp-type echo-reply -j ACCEPT 70 -A Firewall-INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT 71 -A Firewall-INPUT -p icmp --icmp-type time-exceeded -j ACCEPT 72 # Ping 73 -A Firewall-INPUT -p icmp --icmp-type echo-request -j ACCEPT 74 # Accept any established connections 75 -A Firewall-INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 76 # Enable the traffic between the nodes of the cluster 77 -A Firewall-INPUT -s $node_ips -j ACCEPT 78 # Allow connections from docker container 79 -A Firewall-INPUT -i docker0 -j ACCEPT 80 # Accept ssh, http, https and git 81 -A Firewall-INPUT -m conntrack --ctstate NEW -m multiport$multiport_private -p tcp --dports 22,2222,80,443 -j ACCEPT 82 # Log and drop everything else 83 -A Firewall-INPUT -j REJECT 84 COMMIT 85 """ 86 87 multiport_private = ' -s 192.168.0.0/16' if private else '' 88 89 rules_template = string.Template(rules_template_text) 90 return rules_template.substitute(node_ips=string.join(node_ips, ','), multiport_private=multiport_private) 91 92 93 def apply_rules_to_all(host_ips, rules, private_key): 94 pkey = detect_and_create_private_key(private_key) 95 96 threads = [] 97 for ip in host_ips: 98 t = Thread(target=apply_rules, args=(ip, rules, pkey)) 99 t.setDaemon(False) 100 t.start() 101 threads.append(t) 102 for thread in threads: 103 thread.join() 104 105 106 def detect_and_create_private_key(private_key): 107 private_key_text = private_key.read() 108 private_key.seek(0) 109 if '-----BEGIN RSA PRIVATE KEY-----' in private_key_text: 110 return paramiko.RSAKey.from_private_key(private_key) 111 elif '-----BEGIN DSA PRIVATE KEY-----' in private_key_text: 112 return paramiko.DSSKey.from_private_key(private_key) 113 else: 114 raise ValueError('Invalid private key file ' + private_key.name) 115 116 117 def apply_rules(host_ip, rules, private_key): 118 # connect to the server via ssh 119 ssh = paramiko.SSHClient() 120 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 121 ssh.connect(host_ip, username='core', allow_agent=False, look_for_keys=False, pkey=private_key) 122 123 # copy the rules to the temp directory 124 temp_file = '/tmp/' + str(uuid.uuid4()) 125 126 ssh.open_sftp() 127 sftp = ssh.open_sftp() 128 sftp.open(temp_file, 'w').write(rules) 129 130 # move the rules in to place and enable and run the iptables-restore.service 131 commands = [ 132 'sudo mv ' + temp_file + ' /var/lib/iptables/rules-save', 133 'sudo chown root:root /var/lib/iptables/rules-save', 134 'sudo systemctl enable iptables-restore.service', 135 'sudo systemctl start iptables-restore.service' 136 ] 137 138 for command in commands: 139 stdin, stdout, stderr = ssh.exec_command(command) 140 stdout.channel.recv_exit_status() 141 142 ssh.close() 143 144 log_success('Applied rule to ' + host_ip) 145 146 147 def main(): 148 colorama.init() 149 150 parser = argparse.ArgumentParser(description='Apply a "Security Group" to a Deis cluster') 151 parser.add_argument('--private-key', required=True, type=file, dest='private_key', help='Cluster SSH Private Key') 152 parser.add_argument('--private', action='store_true', dest='private', help='Only allow access to the cluster from the private network') 153 parser.add_argument('--discovery-url', dest='discovery_url', help='Etcd discovery url') 154 parser.add_argument('--hosts', nargs='+', dest='hosts', help='The IP addresses of the hosts to apply rules to') 155 args = parser.parse_args() 156 157 nodes = get_nodes_from_args(args) 158 hosts = args.hosts if args.hosts is not None else nodes 159 160 node_ips = [] 161 for ip in nodes: 162 if validate_ip_address(ip): 163 node_ips.append(ip) 164 else: 165 log_warning('Invalid IP will not be added to security group: ' + ip) 166 167 if not len(node_ips) > 0: 168 raise ValueError('No valid IP addresses in security group.') 169 170 host_ips = [] 171 for ip in hosts: 172 if validate_ip_address(ip): 173 host_ips.append(ip) 174 else: 175 log_warning('Host has invalid IP address: ' + ip) 176 177 if not len(host_ips) > 0: 178 raise ValueError('No valid host addresses.') 179 180 log_info('Generating iptables rules...') 181 rules = get_firewall_contents(node_ips, args.private) 182 log_success('Generated rules:') 183 log_debug(rules) 184 185 log_info('Applying rules...') 186 apply_rules_to_all(host_ips, rules, args.private_key) 187 log_success('Done!') 188 189 190 def log_debug(message): 191 print(Style.DIM + Fore.MAGENTA + message + Fore.RESET + Style.RESET_ALL) 192 193 194 def log_info(message): 195 print(Fore.CYAN + message + Fore.RESET) 196 197 198 def log_warning(message): 199 print(Fore.YELLOW + message + Fore.RESET) 200 201 202 def log_success(message): 203 print(Style.BRIGHT + Fore.GREEN + message + Fore.RESET + Style.RESET_ALL) 204 205 206 def log_error(message): 207 print(Style.BRIGHT + Fore.RED + message + Fore.RESET + Style.RESET_ALL) 208 209 if __name__ == "__main__": 210 try: 211 main() 212 except Exception as e: 213 log_error(e.message) 214 sys.exit(1)