class Terrafying::Components::LetsEncrypt
Attributes
name[R]
source[R]
Public Class Methods
create(name, bucket, options = {})
click to toggle source
# File lib/terrafying/components/letsencrypt.rb, line 13 def self.create(name, bucket, options = {}) LetsEncrypt.new.create name, bucket, options end
find(name, bucket, options = {})
click to toggle source
# File lib/terrafying/components/letsencrypt.rb, line 16 def self.find(name, bucket, options = {}) LetsEncrypt.new.find name, bucket, options end
new()
click to toggle source
Calls superclass method
# File lib/terrafying/components/letsencrypt.rb, line 20 def initialize super @acme_providers = setup_providers @zones = [] end
Public Instance Methods
create(name, bucket, options = {})
click to toggle source
# File lib/terrafying/components/letsencrypt.rb, line 41 def create(name, bucket, options = {}) options = { prefix: '', provider: :staging, email_address: 'cloud@uswitch.com', public_certificate: false, curve: 'P384', rsa_bits: '3072', use_external_dns: false, renewing: false, renew_alert_options: { protocol: nil, endpoint: nil, endpoint_auto_confirms: false, confirmation_timeout_in_minutes: 1, raw_message_delivery: false, filter_policy: nil, delivery_policy: nil } }.merge(options) @name = name @bucket = bucket @prefix = options[:prefix] @acme_provider = @acme_providers[options[:provider]] @use_external_dns = options[:use_external_dns] @renewing = options[:renewing] @renew_alert_options = options[:renew_alert_options] @prefix_path = [@prefix, @name].reject(&:empty?).join("/") renew() if @renewing renew_alert() if @renew_alert_options[:endpoint] != nil provider :tls, {} resource :tls_private_key, "#{@name}-account", algorithm: "RSA", rsa_bits: options[:rsa_bits] resource :acme_registration, "#{@name}-reg", provider: @acme_provider[:ref], account_key_pem: output_of(:tls_private_key, "#{@name}-account", 'private_key_pem'), email_address: options[:email_address] @account_key = output_of(:acme_registration, "#{@name}-reg", 'account_key_pem') resource :aws_s3_bucket_object, "#{@name}-account", bucket: @bucket, key: File.join('', @prefix, @name, 'account.key'), content: @account_key resource :aws_s3_bucket_object, "#{@name}-config", { bucket: @bucket, key: File.join('', @prefix, @name, "config.json"), content: { id: output_of(:acme_registration, "#{@name}-reg", "id"), url: @acme_provider[:url], email_address: options[:email_address], }.to_json, } @ca_cert_acl = options[:public_certificate] ? 'public-read' : 'private' open(@acme_provider[:ca_cert], 'rb') do |cert| @ca_cert = cert.read end resource :aws_s3_bucket_object, object_name(@name, :cert), bucket: @bucket, key: object_key(@name, :cert), content: @ca_cert, acl: @ca_cert_acl @source = object_url(@name, :cert) resource :aws_s3_bucket_object, "#{@name}-metadata", bucket: @bucket, key: File.join('', @prefix, @name, '.metadata'), content: { provider: options[:provider].to_s, public_certificate: options[:public_certificate], use_external_dns: options[:use_external_dns], }.to_json self end
create_keypair_in(ctx, name, options = {})
click to toggle source
# File lib/terrafying/components/letsencrypt.rb, line 156 def create_keypair_in(ctx, name, options = {}) options = { common_name: name, organization: "uSwitch Limited", dns_names: [], ip_addresses: [], curve: "P384" }.merge(options) @zones << options[:zone] if options[:zone] key_ident = "#{@name}-#{tf_safe(name)}" ctx.resource :tls_private_key, key_ident, algorithm: 'ECDSA', ecdsa_curve: options[:curve] ctx.resource :tls_cert_request, key_ident, key_algorithm: 'ECDSA', private_key_pem: output_of(:tls_private_key, key_ident, :private_key_pem), subject: { common_name: options[:common_name], organization: options[:organization] }, dns_names: options[:dns_names], ip_addresses: options[:ip_addresses] cert_options = {} cert_options[:recursive_nameservers] = ['1.1.1.1:53', '8.8.8.8:53', '8.8.4.4:53'] if @use_external_dns @renewing ? min_days_remaining = -1 : min_days_remaining = 21 # we don't want Terraform to renew certs if the certbot lambda is provisioned ctx.resource :acme_certificate, key_ident, { provider: @acme_provider[:ref], account_key_pem: @account_key, min_days_remaining: min_days_remaining, dns_challenge: { provider: 'route53' }, certificate_request_pem: output_of(:tls_cert_request, key_ident, :cert_request_pem) }.merge(cert_options) csr_version = "${sha256(tls_cert_request.#{key_ident}.cert_request_pem)}" ctx.resource :aws_s3_bucket_object, "#{key_ident}-csr", bucket: @bucket, key: object_key(name, :csr, csr_version), content: output_of(:tls_cert_request, key_ident, :cert_request_pem) ctx.resource :aws_s3_bucket_object, "#{key_ident}-csr-latest", bucket: @bucket, key: object_key(name, :csr, 'latest'), content: csr_version key_version = "${sha256(tls_private_key.#{key_ident}.private_key_pem)}" ctx.resource :aws_s3_bucket_object, "#{key_ident}-key", bucket: @bucket, key: object_key(name, :key, key_version), content: output_of(:tls_private_key, key_ident, :private_key_pem) ctx.resource :aws_s3_bucket_object, "#{key_ident}-key-latest", bucket: @bucket, key: object_key(name, :key, 'latest'), content: key_version cert_version = "${sha256(acme_certificate.#{key_ident}.certificate_pem)}" cert_config = { bucket: @bucket, key: object_key(name, :cert, cert_version), content: output_of(:acme_certificate, key_ident, :certificate_pem).to_s + @ca_cert, } cert_config[:lifecycle] = { ignore_changes: [ "content" ] } if @renewing ctx.resource :aws_s3_bucket_object, "#{key_ident}-cert", cert_config ctx.resource :aws_s3_bucket_object, "#{key_ident}-cert-latest", bucket: @bucket, key: object_key(name, :cert, 'latest'), content: cert_version reference_keypair(ctx, name, key_version: key_version, cert_version: cert_version) end
find(name, bucket, prefix: "")
click to toggle source
# File lib/terrafying/components/letsencrypt.rb, line 128 def find(name, bucket, prefix: "") @name = name @bucket = bucket @prefix = prefix # load the rest of the config from an s3 metadata file metadata_obj = aws.s3_object(@bucket, [@prefix, @name, '.metadata'].compact.reject(&:empty?).join('/')) metadata = JSON.parse(metadata_obj, symbolize_names: true) @acme_provider = @acme_providers[metadata[:provider].to_sym] @use_external_dns = metadata[:use_external_dns] @ca_cert_acl = metadata[:public_certificate] ? 'public-read' : 'private' account_key_obj = data :aws_s3_bucket_object, "#{@name}-account", bucket: @bucket, key: File.join('', @prefix, @name, 'account.key') @account_key = account_key_obj["body"] open(@acme_provider[:ca_cert], 'rb') do |cert| @ca_cert = cert.read end @source = object_url(@name, :cert) self end
generate_alpha_num()
click to toggle source
# File lib/terrafying/components/letsencrypt.rb, line 423 def generate_alpha_num() result = @name.split("").each do |ch| alpha_num = ch.upcase.ord - 'A'.ord return alpha_num.abs if (alpha_num.abs < 24) end result.is_a?(Integer) ? result : 6 end
output_with_children()
click to toggle source
Calls superclass method
# File lib/terrafying/components/letsencrypt.rb, line 241 def output_with_children iam_policy = {} if @renewing iam_policy = resource :aws_iam_policy, "#{@name}_lambda_execution_policy", { name: "#{@name}_lambda_execution_policy", description: "A policy for the #{@name}_lambda function to access S3 and R53", policy: JSON.pretty_generate( { Version: "2012-10-17", Statement: [ { Action: [ "s3:Put*", "s3:Get*", "s3:DeleteObject" ], Resource: [ "arn:aws:s3:::#{@bucket}/#{@prefix_path}/*" ], Effect: "Allow" }, { Action: [ "s3:ListBucket" ], Resource: [ "arn:aws:s3:::#{@bucket}" ], Effect: "Allow" }, { Action: [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], Resource: [ "arn:aws:logs:*:*:*" ], Effect: "Allow" }, { Action: [ "route53:ListHostedZones", ], Resource: [ "*" ], Effect: "Allow" }, { Action: [ "route53:GetChange", ], Resource: [ "arn:aws:route53:::change/*" ], Effect: "Allow" }, { Action: [ "route53:ChangeResourceRecordSets", ], Resource: @zones.compact.map { | zone | "arn:aws:route53:::#{zone.id[1..-1]}" }, Effect: "Allow" } ] } ) } end super end
renew()
click to toggle source
# File lib/terrafying/components/letsencrypt.rb, line 318 def renew execution_role = resource :aws_iam_role, "#{@name}_lambda_execution", { name: "#{@name}_lambda_execution", assume_role_policy: JSON.pretty_generate( { Version: "2012-10-17", Statement: [ { Action: "sts:AssumeRole", Principal: { Service: "lambda.amazonaws.com" }, Effect: "Allow", Sid: "" } ] } ) } lambda_function = resource :aws_lambda_function, "#{@name}_lambda", { function_name: "#{@name}_lambda", s3_bucket: "uswitch-certbot-lambda", s3_key: "certbot-lambda.zip", handler: "main.handler", runtime: "python3.7", timeout: "900", role: execution_role["arn"], environment:{ variables: { CA_BUCKET: @bucket, CA_PREFIX: @prefix_path } } } resource :aws_iam_role_policy_attachment, "#{@name}_lambda_policy_attachment", { role: execution_role["name"], policy_arn: "${aws_iam_policy.#{@name}_lambda_execution_policy.arn}" } alpha_num = generate_alpha_num().to_s event_rule = resource :aws_cloudwatch_event_rule, "once_per_day", { name: "once-per-day", description: "Fires once per day", schedule_expression: "cron(0 #{alpha_num} * * ? *)" } resource :aws_cloudwatch_event_target, "#{@name}_lambda_event_target", { rule: event_rule["name"], target_id: lambda_function["id"], arn: lambda_function["arn"] } resource :aws_lambda_permission, "allow_cloudwatch_to_invoke_#{@name}_lambda", { statement_id: "AllowExecutionFromCloudWatch", action: "lambda:InvokeFunction", function_name: lambda_function["function_name"], principal: "events.amazonaws.com", source_arn: event_rule["arn"] } self end
renew_alert()
click to toggle source
# File lib/terrafying/components/letsencrypt.rb, line 383 def renew_alert topic = resource :aws_sns_topic, "#{@name}_lambda_cloudwatch_topic", { name: "#{@name}_lambda_cloudwatch_topic" } alarm = resource :aws_cloudwatch_metric_alarm, "#{@name}_lambda_failure_alarm", { alarm_name: "#{@name}-lambda-failure-alarm", comparison_operator: "GreaterThanOrEqualToThreshold", evaluation_periods: "1", period: "300", metric_name: "Errors", namespace: "AWS/Lambda", threshold: 1, statistic: "Maximum", alarm_description: "Alert generated if the #{@name} certbot lambda fails execution", actions_enabled: true, dimensions: { FunctionName: "${aws_lambda_function.#{@name}_lambda.function_name}" }, alarm_actions: [ "${aws_sns_topic.#{@name}_lambda_cloudwatch_topic.arn}" ], ok_actions: [ "${aws_sns_topic.#{@name}_lambda_cloudwatch_topic.arn}" ] } subscription = resource :aws_sns_topic_subscription, "#{@name}_lambda_cloudwatch_subscription", { topic_arn: "${aws_sns_topic.#{@name}_lambda_cloudwatch_topic.arn}", protocol: @renew_alert_options[:protocol], endpoint: @renew_alert_options[:endpoint], endpoint_auto_confirms: @renew_alert_options[:endpoint_auto_confirms], confirmation_timeout_in_minutes: @renew_alert_options[:confirmation_timeout_in_minutes], raw_message_delivery: @renew_alert_options[:raw_message_delivery], filter_policy: @renew_alert_options[:filter_policy], delivery_policy: @renew_alert_options[:delivery_policy] } self end
setup_providers()
click to toggle source
# File lib/terrafying/components/letsencrypt.rb, line 26 def setup_providers { staging: { url: 'https://acme-staging-v02.api.letsencrypt.org/directory', ref: provider(:acme, alias: :staging, server_url: 'https://acme-staging-v02.api.letsencrypt.org/directory'), ca_cert: 'https://letsencrypt.org/certs/fakeleintermediatex1.pem' }, live: { url: 'https://acme-v02.api.letsencrypt.org/directory', ref: provider(:acme, alias: :live, server_url: 'https://acme-v02.api.letsencrypt.org/directory'), ca_cert: 'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt' } } end