class MU::Cloud::Google::FirewallRule
A firewall ruleset as configured in {MU::Config::BasketofKittens::firewall_rules}
Constants
- PROTOS
Firewall protocols supported by GCP as of early 2019
- STD_PROTOS
Our default subset of supported firewall protocols
Attributes
Public Class Methods
Remove all security groups (firewall rulesets) associated with the currently loaded deployment. @param noop [Boolean]: If true, will only print what would be done @param ignoremaster [Boolean]: If true, will remove resources not flagged as originating from this Mu server @return [void]
# File modules/mu/providers/google/firewall_rule.rb, line 210 def self.cleanup(noop: false, deploy_id: MU.deploy_id, ignoremaster: false, credentials: nil, flags: {}) flags["habitat"] ||= MU::Cloud::Google.defaultProject(credentials) return if !MU::Cloud.resourceClass("Google", "Habitat").isLive?(flags["habitat"], credentials) filter = %Q{(labels.mu-id = "#{MU.deploy_id.downcase}")} if !ignoremaster and MU.mu_public_ip filter += %Q{ AND (labels.mu-master-ip = "#{MU.mu_public_ip.gsub(/\./, "_")}")} end MU.log "Placeholder: Google FirewallRule artifacts do not support labels, so ignoremaster cleanup flag has no effect", MU::DEBUG, details: filter MU::Cloud::Google.compute(credentials: credentials).delete( "firewall", flags["habitat"], nil, noop ) end
Locate and return cloud provider descriptors of this resource type which match the provided parameters, or all visible resources if no filters are specified. At minimum, implementations of find
must honor credentials
and cloud_id
arguments. We may optionally support other search methods, such as tag_key
and tag_value
, or cloud-specific arguments like project
. See also {MU::MommaCat.findStray}. @param args [Hash]: Hash
of named arguments passed via Ruby's double-splat @return [Hash<String,OpenStruct>]: The cloud provider's complete descriptions of matching resources
# File modules/mu/providers/google/firewall_rule.rb, line 174 def self.find(**args) args = MU::Cloud::Google.findLocationArgs(args) found = {} resp = begin MU::Cloud::Google.compute(credentials: args[:credentials]).list_firewalls(args[:project]) rescue ::Google::Apis::ClientError => e raise e if !e.message.match(/^(?:notFound|forbidden): /) end if resp and resp.items resp.items.each { |fw| next if !args[:cloud_id].nil? and fw.name != args[:cloud_id] found[fw.name] = fw } end found end
Does this resource type exist as a global (cloud-wide) artifact, or is it localized to a region/zone? @return [Boolean]
# File modules/mu/providers/google/firewall_rule.rb, line 196 def self.isGlobal? true end
Initialize this cloud resource object. Calling super
will invoke the initializer defined under {MU::Cloud}, which should set the attribtues listed in {MU::Cloud::PUBLIC_ATTRS} as well as applicable dependency shortcuts, like @vpc
, for us. @param args [Hash]: Hash
of named arguments passed via Ruby's double-splat
# File modules/mu/providers/google/firewall_rule.rb, line 32 def initialize(**args) super if !@vpc.nil? @mu_name ||= @deploy.getResourceName(@config['name'], need_unique_string: true, max_length: 61) else @mu_name ||= @deploy.getResourceName(@config['name'], max_length: 61) end end
Denote whether this resource implementation is experiment, ready for testing, or ready for production use.
# File modules/mu/providers/google/firewall_rule.rb, line 202 def self.quality MU::Cloud::RELEASE end
Cloud-specific configuration properties. @param _config [MU::Config]: The calling MU::Config
object @return [Array<Array,Hash>]: List of required fields, and json-schema Hash
of cloud-specific configuration parameters for this resource
# File modules/mu/providers/google/firewall_rule.rb, line 370 def self.schema(_config = nil) toplevel_required = [] schema = { "rules" => { "items" => { "properties" => { "weight" => { "type" => "integer", "description" => "Explicitly set a priority for this firewall rule, between 0 and 65535, with lower numbered priority rules having greater precedence." }, "deny" => { "type" => "boolean", "default" => false, "description" => "Set this rule to +DENY+ traffic instead of +ALLOW+" }, "proto" => { "description" => "The protocol to allow with this rule. The +standard+ keyword will expand to a series of identical rules covering +icmp+, +tcp+, and +udp; the +all+ keyword will expand to a series of identical rules for all supported protocols.", "enum" => PROTOS + ["all", "standard"] }, "source_tags" => { "type" => "array", "description" => "VMs with these tags, from which traffic will be allowed", "items" => { "type" => "string" } }, "source_service_accounts" => { "type" => "array", "description" => "Resources using these service accounts, from which traffic will be allowed", "items" => { "type" => "string" } }, "target_tags" => { "type" => "array", "description" => "VMs with these tags, to which traffic will be allowed", "items" => { "type" => "string" } }, "target_service_accounts" => { "type" => "array", "description" => "Resources using these service accounts, to which traffic will be allowed", "items" => { "type" => "string" } } } } }, "project" => { "type" => "string", "description" => "The project into which to deploy resources" } } [toplevel_required, schema] end
Cloud-specific pre-processing of {MU::Config::BasketofKittens::firewall_rules}, bare and unvalidated. @param acl [Hash]: The resource to process and validate @param config [MU::Config]: The overall deployment config of which this resource is a member @return [Boolean]: True if validation succeeded, False otherwise
# File modules/mu/providers/google/firewall_rule.rb, line 432 def self.validateConfig(acl, config) ok = true if acl['vpc'] if !acl['vpc']['habitat'] acl['vpc']['project'] ||= acl['project'] elsif acl['vpc']['habitat'] and acl['vpc']['habitat']['id'] acl['vpc']['project'] = acl['vpc']['habitat']['id'] elsif acl['vpc']['habitat'] and acl['vpc']['habitat']['name'] acl['vpc']['project'] = acl['vpc']['habitat']['name'] end correct_vpc = MU::Cloud.resourceClass("Google", "VPC").pickVPC( acl['vpc'], acl, "firewall_rule", config ) acl['vpc'] = correct_vpc if correct_vpc end acl['rules'] ||= [] # Firewall entries without rules are illegal in GCP, so insert a # default-deny placeholder. if acl['rules'].empty? acl['rules'] << { "deny" => true, "proto" => "all", "hosts" => ["0.0.0.0/0"], "weight" => 65535 } end # First, expand some of our protocol shorthand into a real list append = [] delete = [] acl['rules'].each { |r| if !r['egress'] if !r['source_tags'] and !r['source_service_accounts'] and (!r['hosts'] or r['hosts'].empty?) r['hosts'] = ['0.0.0.0/0'] end else if !r['destination_tags'] and !r['destination_service_accounts'] and (!r['hosts'] or r['hosts'].empty?) r['hosts'] = ['0.0.0.0/0'] end end if r['proto'] == "standard" STD_PROTOS.each { |p| newrule = r.dup newrule['proto'] = p append << newrule } delete << r elsif r['proto'] == "all" PROTOS.each { |p| newrule = r.dup newrule['proto'] = p append << newrule } delete << r end } delete.each { |r| acl['rules'].delete(r) } acl['rules'].concat(append) # Next, bucket these by what combination of allow/deny and # ingress/egress rule they are. If we have more than one # classification rules_by_class = { "allow-ingress" => [], "allow-egress" => [], "deny-ingress" => [], "deny-egress" => [], } acl['rules'].each { |rule| if rule['deny'] if rule['egress'] rules_by_class["deny-egress"] << rule else rules_by_class["deny-ingress"] << rule end else if rule['egress'] rules_by_class["allow-egress"] << rule else rules_by_class["allow-ingress"] << rule end end } rules_by_class.reject! { |_k, v| v.size == 0 } # Generate other firewall rule objects to cover the other behaviors # we've requested, if indeed we've done so. if rules_by_class.size > 1 keep = rules_by_class.keys.first acl['rules'] = rules_by_class[keep] rules_by_class.delete(keep) rules_by_class.each_pair { |behaviors, rules| newrule = acl.dup newrule['name'] += "-"+behaviors newrule['rules'] = rules ok = false if !config.insertKitten(newrule, "firewall_rules") } end ok end
Public Instance Methods
Insert a rule into an existing security group.
@param hosts [Array<String>]: An array of CIDR network addresses to which this rule will apply. @param proto [String]: One of “tcp,” “udp,” or “icmp” @param port [Integer]: A port number. Only valid with udp or tcp. @param egress [Boolean]: Whether this is an egress ruleset, instead of ingress. @param port_range [String]: A port range descriptor (e.g. 0-65535). Only valid with udp or tcp. @return [void]
# File modules/mu/providers/google/firewall_rule.rb, line 163 def addRule(hosts, proto: "tcp", port: nil, egress: false, port_range: "0-65535") end
Called by {MU::Deploy#createResources}
# File modules/mu/providers/google/firewall_rule.rb, line 45 def create @cloud_id = @mu_name.downcase.gsub(/[^-a-z0-9]/, "-") vpc_id = if @vpc @vpc.url if !@vpc.nil? else vpc_ref = MU::Config::Ref.get(@config['vpc']) if !vpc_ref.kitten MU.log "Failed to resolve VPC for FirewallRule #{@mu_name}", MU::ERR, details: @config['vpc'] raise MuError, "Failed to resolve VPC for FirewallRule #{@mu_name}" else vpc_ref.kitten.url end end if vpc_id.nil? raise MuError, "Failed to resolve VPC for #{self}" end params = { :name => @cloud_id, :network => vpc_id } @config['rules'].each { |rule| srcs = [] ruleobj = nil # XXX 'all' and 'standard' keywords if ["tcp", "udp"].include?(rule['proto']) and (rule['port_range'] or rule['port']) ruleobj = MU::Cloud::Google.compute(:Firewall)::Allowed.new( ip_protocol: rule['proto'], ports: [rule['port_range'] || rule['port']] ) else ruleobj = MU::Cloud::Google.compute(:Firewall)::Allowed.new( ip_protocol: rule['proto'] ) end if rule['hosts'] rule['hosts'].each { |cidr| srcs << cidr } end dir = (rule["ingress"] or !rule["egress"]) ? "INGRESS" : "EGRESS" if params[:direction] and params[:direction] != dir MU.log "Google Cloud firewalls cannot mix ingress and egress rules", MU::ERR, details: @config['rules'] raise MuError, "Google Cloud firewalls cannot mix ingress and egress rules" end params[:direction] = dir if @deploy params[:description] = @deploy.deploy_id end filters = if dir == "INGRESS" ['source_service_accounts', 'source_tags'] else ['target_service_accounts', 'target_tags'] end filters.each { |filter| if config[filter] and config[filter].size > 0 params[filter.to_sym] = config[filter].dup end } action = rule['deny'] ? :denied : :allowed params[action] ||= [] params[action] << ruleobj ipparam = dir == "INGRESS" ? :source_ranges : :destination_ranges params[ipparam] ||= [] params[ipparam].concat(srcs) params[:priority] = rule['weight'] if rule['weight'] } fwobj = MU::Cloud::Google.compute(:Firewall).new(params) MU.log "Creating firewall #{@cloud_id} in project #{@project_id}", details: fwobj begin MU::Cloud::Google.compute(credentials: @config['credentials']).insert_firewall(@project_id, fwobj) rescue ::Google::Apis::ClientError => e MU.log @config['project']+"/"+@config['name']+": "+@cloud_id, MU::ERR, details: @config['vpc'] MU.log e.inspect, MU::ERR, details: fwobj if e.message.match(/Invalid value for field/) dependencies(use_cache: false, debug: true) end raise e end # Make sure it actually got made before we move on desc = nil begin desc = MU::Cloud::Google.compute(credentials: @config['credentials']).get_firewall(@project_id, @cloud_id) sleep 1 end while desc.nil? desc end
Called by {MU::Deploy#createResources}
# File modules/mu/providers/google/firewall_rule.rb, line 139 def groom end
Log
metadata about this ruleset to the currently running deployment
# File modules/mu/providers/google/firewall_rule.rb, line 143 def notify sg_data = MU.structToHash( MU::Cloud::Google::FirewallRule.find(cloud_id: @cloud_id, region: @config['region']) ) sg_data ||= {} sg_data["group_id"] = @cloud_id sg_data["project_id"] = habitat_id sg_data["cloud_id"] = @cloud_id return sg_data end
Reverse-map our cloud description into a runnable config hash. We assume that any values we have in +@config+ are placeholders, and calculate our own accordingly based on what's live in the cloud.
# File modules/mu/providers/google/firewall_rule.rb, line 230 def toKitten(**_args) if cloud_desc.name.match(/^[a-f0-9]+$/) gke_ish = true cloud_desc.target_tags.each { |tag| gke_ish = false if !tag.match(/^gke-/) } if gke_ish MU.log "FirewallRule #{cloud_desc.name} appears to belong to a ContainerCluster, skipping adoption", MU::DEBUG return nil end end bok = { "cloud" => "Google", "project" => @config['project'], "credentials" => @config['credentials'] } bok['rules'] = [] bok['name'] = cloud_desc.name.dup bok['cloud_id'] = cloud_desc.name.dup cloud_desc.network.match(/(?:^|\/)projects\/(.*?)\/.*?\/networks\/([^\/]+)(?:$|\/)/) vpc_proj = Regexp.last_match[1] vpc_id = Regexp.last_match[2] if vpc_id == "default" and !@config['project'] raise MuError, "FirewallRule toKitten: I'm in 'default' VPC but can't figure out what project I'm in" end # XXX make sure this is sane (that these rules come with default VPCs) if vpc_id == "default" and ["default-allow-icmp", "default-allow-http"].include?(cloud_desc.name) return nil end if vpc_id != "default" bok['vpc'] = MU::Config::Ref.get( id: vpc_id, habitat: MU::Config::Ref.get( id: vpc_proj, cloud: "Google", credentials: @credentials, type: "habitats" ), cloud: "Google", credentials: @config['credentials'], type: "vpcs" ) end byport = {} rule_list = [] is_deny = false if cloud_desc.denied rule_list = cloud_desc.denied is_deny = true else rule_list = cloud_desc.allowed end rule_list.each { |rule| hosts = if cloud_desc.direction == "INGRESS" cloud_desc.source_ranges ? cloud_desc.source_ranges : ["0.0.0.0/0"] else cloud_desc.destination_ranges ? cloud_desc.destination_ranges : ["0.0.0.0/0"] end hosts.map! { |h| h = h+"/32" if h.match(/^\d+\.\d+\.\d+\.\d+$/) h } proto = rule.ip_protocol ? rule.ip_protocol : "all" if rule.ports rule.ports.each { |ports| ports = "0-65535" if ["1-65535", "1-65536", "0-65536"].include?(ports) byport[ports] ||= {} byport[ports][hosts] ||= [] byport[ports][hosts] << proto } else byport["0-65535"] ||= {} byport["0-65535"][hosts] ||= [] byport["0-65535"][hosts] << proto end } byport.each_pair { |ports, hostlists| hostlists.each_pair { |hostlist, protos| protolist = if protos.sort.uniq == PROTOS.sort.uniq ["all"] elsif protos.sort.uniq == ["icmp", "tcp", "udp"] ["standard"] else protos end protolist.each { |proto| rule = { "proto" => proto, "hosts" => hostlist } rule["deny"] = true if is_deny if cloud_desc.priority and cloud_desc.priority != 1000 rule["weight"] = cloud_desc.priority end if ports.match(/-/) rule["port_range"] = ports else rule["port"] = ports.to_i end if cloud_desc.source_service_accounts rule["source_service_accounts"] = cloud_desc.source_service_accounts end if cloud_desc.source_tags rule["source_tags"] = cloud_desc.source_tags end if cloud_desc.target_service_accounts rule["target_service_accounts"] = cloud_desc.target_service_accounts end if cloud_desc.target_tags rule["target_tags"] = cloud_desc.target_tags end if cloud_desc.direction == "EGRESS" rule['egress'] = true rule['ingress'] = false end bok['rules'] << rule } } } bok end