GithubHelp home page GithubHelp logo

Comments (21)

ScottWales avatar ScottWales commented on July 19, 2024 3

I've worked around this with a custom fact that returns the live certificate directories:

require "pathname"

Facter.add(:letsencrypt_live) do
    setcode do
        Pathname.new("/etc/letsencrypt/live").children.select(&:directory?).collect(&:basename)
    end
end

then in puppet only enabling the ssl server when it's in the list of certificates:

::apache::vhost {"${hostname} nossl": 
    # ...
}
::letsencrypt::certonly {$hostname:
    require => Service[httpd],
    # ...
}
if $hostname in $::letsencrypt_live {
    ::apache::vhost{"${hostname} ssl":
        # ...
    }
}

This does require two runs of Puppet before the SSL host is live

from puppet-letsencrypt.

minorOffense avatar minorOffense commented on July 19, 2024 1

That's a good point. Alright I'm convinced now ;-)

from puppet-letsencrypt.

thiagomarinho avatar thiagomarinho commented on July 19, 2024

@hoffereka I have the same issue, did you solve it?

from puppet-letsencrypt.

benjunmun avatar benjunmun commented on July 19, 2024

I've got it working in a single run, but it's not pretty. This sample code is for ubuntu 16.04. The key parts are running letsencrypt between the ssl and non-ssl vhosts, and restarting apache - the puppet default mpm is different from the ubuntu package's default mpm.

class { ::letsencrypt:
    config     => {
        email  => '[email protected]',
        server => 'https://acme-staging.api.letsencrypt.org/directory',
    }
}

class { ::apache:
    default_vhost => false,
}

#This fixes:
#   AH00534: apache2: Configuration error: The MPM cannot be changed during restart.
#The letsencrypt plugin only sends a SIGUSR1, which is insufficient
exec {'Restart Apache for MPM changes':
    command     => '/bin/systemctl restart apache2.service',
    refreshonly => true,
}

Apache::Vhost<|ssl == false|> ~> Exec['Restart Apache for MPM changes'] -> Letsencrypt::Certonly<||> -> Apache::Vhost<|ssl == true|>

package {'python-letsencrypt-apache':
    ensure => present,
    before => Letsencrypt::Certonly['test.domain.example'],
}

letsencrypt::certonly {'test.domain.example':
    domains         => ['test.domain.example'],
    plugin          => 'apache',
    additional_args => ['--debug'],
}

apache::vhost {'test.domain.example_http':
    servername      => 'test.domain.example',
    port            => 80,
    docroot         => '/var/www',
    redirect_status => 'permanent',
    redirect_dest   => 'https://test.domain.example/',
}

apache::vhost {'test.domain.example_https':
    servername => 'test.domain.example',
    port       => 443,
    docroot    => '/var/www',
    ssl        => true,
    ssl_cert   => '/etc/letsencrypt/live/test.domain.example/fullchain.pem',
    ssl_key    => '/etc/letsencrypt/live/test.domain.example/privkey.pem',
}

from puppet-letsencrypt.

minorOffense avatar minorOffense commented on July 19, 2024

I vote that this fact be added to the module proper. It'd be useful in various ways.

#66 (comment)

from puppet-letsencrypt.

alexjfisher avatar alexjfisher commented on July 19, 2024

@minorOffense Would you be able to create a PR?

from puppet-letsencrypt.

pgassmann avatar pgassmann commented on July 19, 2024

the fact returns a hash of live certificates. Cool.

Bonus point if the fact returns certificate information like domains, validity etc.

from puppet-letsencrypt.

minorOffense avatar minorOffense commented on July 19, 2024

Yeah I can create a PR. I'd just need to adjust the fact in case that directory isn't the same on all systems.

As for validity, domains, etc. that may be unnecessary since your ssl definition would have some of that info. I'll start with just the basic fact, see where that leads me.

from puppet-letsencrypt.

pgassmann avatar pgassmann commented on July 19, 2024

additional information could be useful for multi-domain certificates. then the check if the certificate is available is not enough. The additional domain could be missing.

from puppet-letsencrypt.

cwells avatar cwells commented on July 19, 2024

I had to modify the fact to get it to work:

require 'pathname'

Facter.add(:letsencrypt_live) do
    setcode do
        livedir = Pathname.new('/etc/letsencrypt/live')
        if !livedir.directory?
            []
        else
            (livedir.children
                .select(&:directory?)
                .collect(&:basename)
                .map { |name| name.to_s })
        end
    end
end

Without the final map, the array contains pathname objects rather than strings:

[root@rev-ru ~]# /opt/puppetlabs/puppet/bin/irb 
irb(main):001:0> require 'pathname'
=> true
irb(main):002:0> Pathname.new("/etc/letsencrypt/live").children.select(&:directory?).collect(&:basename)
=> [#<Pathname:rev-ru.aws.focusvision.com>]
irb(main):003:0> Pathname.new("/etc/letsencrypt/live").children.select(&:directory?).collect(&:basename).map { |name| name.to_s }
=> ["rev-ru.aws.focusvision.com"]

[edit] can't return from setcode... fixed.

from puppet-letsencrypt.

ekohl avatar ekohl commented on July 19, 2024

It's good to note that certbot can also operate from /etc/certbot instead of /etc/letsencrypt but will prefer the latter if it exists. At least on my Debian box.

from puppet-letsencrypt.

cwells avatar cwells commented on July 19, 2024

Something like this could implement the behavior @ekohl describes:

require 'pathname'

Facter.add(:letsencrypt_live) do
    setcode do
        livedir = ([ '/etc/letsencrypt/live', '/etc/certbot/live' ]
            .map { |path| Pathname.new path }
            .find(&:directory?))

        if livedir.nil? then []
        else (livedir.children
            .select(&:directory?)
            .collect(&:basename)
            .map(&:to_s))
        end
    end
end

Sorry about multiple edits... Ruby is not my primary language.

from puppet-letsencrypt.

ekohl avatar ekohl commented on July 19, 2024

@cwells I think you can also use .find() instead of .select()[0]

from puppet-letsencrypt.

cwells avatar cwells commented on July 19, 2024

@ekohl updated, thanks.

from puppet-letsencrypt.

cwells avatar cwells commented on July 19, 2024

Okay, per @pgassmann's comment, this fact generates a hash indexed by SAN, referencing parent directory of certificate.

require 'openssl'
require 'pathname'

Facter.add(:letsencrypt_directory) do
    setcode do
        certs = {}

        # locate the certificate repository
        livedir = ([ '/etc/letsencrypt/live', '/etc/certbot/live' ]
            .map { |path| Pathname.new path }
            .find(&:directory?))

        Pathname.new(livedir).children.select(&:directory?).each do |path|
            pem = File.join(path, 'cert.pem')
            cert = OpenSSL::X509::Certificate.new(File.new(pem).read)
            san = cert.extensions.find { |e| e.oid == 'subjectAltName' }
            names = san.value.split(',').map { |entry| entry.split(':')[1] }
            names.each do |n|
                certs[n] = path.to_s
            end
        end unless livedir.nil?

        certs
    end
end

returns something like:

{ 
    'san1.domain.com' => '/etc/letsencrypt/live/san.domain.com',
    'san2.domain.com' => '/etc/letsencrypt/live/san.domain.com',
    'www.domain.com' => '/etc/letsencrypt/live/www.domain.com'
 }

from puppet-letsencrypt.

cwells avatar cwells commented on July 19, 2024

... and a companion function that will lookup a CN (and also match wildcard certs).

Puppet::Functions.create_function(:letsencrypt_lookup) do
    def letsencrypt_lookup(cn)
        domain = cn.split('.', 2)[1]
        wildcard = "*.#{domain}"
        certs = closure_scope['facts'].fetch('letsencrypt_directory', nil)
        certs.fetch(cn, certs.fetch(wildcard, nil)) unless certs.nil?
    end
end

So now in your manifest, you can write:

    $cert = letsencrypt_lookup($hostname)
    if $cert {
        nginx::resource::server { "ssl-${hostname}":
            ssl         => true,
            ssl_port    => 443,
            ssl_cert    => "${cert}/fullchain.pem",
            ssl_key     => "${cert}/privkey.pem"
            # ...
        }
    }

from puppet-letsencrypt.

cmseal avatar cmseal commented on July 19, 2024

Hi - I'm still a bit of a Puppet n00b, but I'm looking to do the same thing using nginx instead of apache, using the Vox nginx module. Can the same theory be applied as above from @benjunmun ?
Help/suggestions much appreciated! :)

from puppet-letsencrypt.

pgassmann avatar pgassmann commented on July 19, 2024

@cmseal Take a look at my module letsencrypt_nginx for reference or use it directly

from puppet-letsencrypt.

minorOffense avatar minorOffense commented on July 19, 2024

I've been testing the fact for seeing if the cert is live or not. I'm still not convinced that all the extra metadata about the certs is useful in puppet (or the extra processing time to generate those facts especially with lots of certs).

But here's a reference implementation of an apache::vhosts class we've started using which uses the self-signed certs until the signed ones become available. This only works with Puppet 5 and the latest apache modules.

The way it works is you pass a custom boolean option called "manage_cert" through your vhost. It then creates the redirect from http to https, sets the SSL cert location, cleans up some config and then hands it off to the apache module to actually create the configuration.

Works great, within two puppet runs you have valid SSL certificates in place. Any suggestions or improvements are welcome.

In any case I'll be prepping the merge request to add the fact about the certs.

# Wrapper class that adds automatic SSL management.
class coldfront::apache::vhosts {
  include ::apache::vhosts # Also allows for backwards compatibility to dropfort vms from the v3 provisioner.

  # Grab the defined vhosts.
  $vhosts = lookup('coldfront::apache::vhosts', {value_type => Hash, merge => deep, default_value => {}})

  # Scan for SSL enabled vhosts.
  $auto_ssl = $vhosts.filter |$vhost_indexes, $vhost_values| {
    $vhost_values['ssl'] == true and $vhost_values['manage_cert'] == true
  }

  # Only require letsencrypt if the auto signing is requested
  if ! empty($auto_ssl) {
    include ::letsencrypt
  }

  # Create the certs
  $auto_ssl.each |$key, $value| {
    # @todo make the system command platform agnostic.
    ::letsencrypt::certonly {"${value['servername']}":
      manage_cron => true,
      plugin => 'webroot',
      webroot_paths => ['/var/www/html'],
      suppress_cron_output => true,
      cron_success_command => '/bin/systemctl graceful httpd',
    }
  }

  # Create redirects from insecure port.
  $auto_redirect_array = $auto_ssl.map |$index, $value| {
    {"insecure-${index}" => {
        'servername' => $value['servername'],
        'docroot' => $value['docroot'],
        'port' => 80,
        'proxy_pass' => [
          {'path' => '/.well-known/acme-challenge/', 'url' => '!'},
        ],
        'aliases' => [
          {
            'alias' => '/.well-known/acme-challenge/',
            'path' => '/var/www/html/.well-known/acme-challenge/'
            },
        ],
        'directories' => [
          {
            'path' => '/var/www/html/.well-known/acme-challenge/',
            'allow'=> 'from all',
            'allow_override' => 'None',
            'options' => [
              'Indexes',
              'FollowSymLinks',
              'MultiViews'
            ],
          },
        ],
        'rewrites' => [
          {
            'rewrite_cond' => ['%{REQUEST_URI} !\.well-known/acme-challenge'],
            'rewrite_rule' => ['^(.*) https://%{SERVER_NAME}$1 [R=301,L]'],
          },
        ],
      }
    }
  }

  # Convert to hash.
  $auto_redirect = $auto_redirect_array.reduce |$memo, $value| {
    $memo + $value
  }

  # Get the list of certs that are live.
  $auto_ssl_live = $auto_ssl.filter |$live_indexes, $live_values| {$live_values['servername'] in $::letsencrypt_live}

  # Update the cert info for each live vhost.
  $auto_ssl_vhosts_array = $auto_ssl_live.map |$index, $values| {
    {$index => deep_merge($values,
      {
        'ssl_cert' => "/etc/letsencrypt/live/${values['servername']}/cert.pem",
        'ssl_key' => "/etc/letsencrypt/live/${values['servername']}/privkey.pem",
        'ssl_chain' => "/etc/letsencrypt/live/${values['servername']}/chain.pem",
      }
    )}
  }
  #https://github.com/puppetlabs/puppetlabs-stdlib#deep_merge

  # Convert map array to hash.
  $auto_ssl_vhosts = $auto_ssl_vhosts_array.reduce |$memo, $value| {
    $memo + $value
  }

  ## We'll need a few layers of merges here.
  # 1. Combine the automatic ssl hosts
  # 2. Combine the automatic ssl hosts with valid certs
  # 3. Combine the automatic ssl redirect hosts
  # 4. Combine all the aliased insecure hosts with the main list.
  # 4. Combine the untouched vhosts with the updated settings

  $managed_ssl_vhosts  = deep_merge($auto_ssl, $auto_ssl_vhosts)
  $managed_vhosts      = deep_merge($vhosts, $auto_redirect)
  $processed_vhosts    = deep_merge($managed_vhosts, $managed_ssl_vhosts)

  # Delete custom vhost options.
  $cleaned_vhosts_array = $processed_vhosts.map |$processed_index, $processed_values| {
    {$processed_index => delete($processed_values,  ['manage_cert'])}
  }

  # Convert map array to hash.
  $cleaned_vhosts = $cleaned_vhosts_array.reduce |$memo, $value| {
    $memo + $value
  }

  if ! empty($cleaned_vhosts) {
    create_resources('::apache::vhost', $cleaned_vhosts)
  }
}

from puppet-letsencrypt.

cwells avatar cwells commented on July 19, 2024

@minorOffense "I'm still not convinced that all the extra metadata about the certs is useful in puppet (or the extra processing time to generate those facts especially with lots of certs)."

The "extra" metadata is a list of subject alternative names (SAN) listed in the certificate (including wildcards). You cannot determine which domains a certificate is valid for simply by checking the filename, as the filename is only based on the common name (CN) and there may be additional names present within the cert.

from puppet-letsencrypt.

Dan33l avatar Dan33l commented on July 19, 2024

The initial question no longer seems relevant.
I am using this Puppet module with Apache without issues.
Feel free to reopen if needed.

from puppet-letsencrypt.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.