├── .fixtures.yml ├── .gitignore ├── .msync.yml ├── .nodeset.yml ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── README.md ├── Rakefile ├── examples └── init.pp ├── files └── letsencrypt_check_altnames.sh ├── lib ├── facter │ ├── letsencrypyt_certs.rb │ ├── letsencrypyt_crt.rb │ └── letsencrypyt_csr.rb └── puppet │ └── parser │ └── functions │ └── file_or_empty_string.rb ├── manifests ├── certificate.pp ├── csr.pp ├── deploy.pp ├── deploy │ └── crt.pp ├── init.pp ├── params.pp ├── request.pp ├── request │ ├── crt.pp │ ├── handler.pp │ └── ocsp.pp ├── setup.pp └── setup │ └── puppetmaster.pp ├── metadata.json ├── spec ├── classes │ └── letsencrypt_init_spec.rb └── spec_helper.rb └── templates ├── cert.cnf.erb ├── letsencrypt.conf.erb ├── letsencrypt_get_certificate_chain.sh.erb └── letsencrypt_get_certificate_ocsp.sh.erb /.fixtures.yml: -------------------------------------------------------------------------------- 1 | fixtures: 2 | repositories: 3 | stdlib: 4 | repo: 'git://github.com/puppetlabs/puppetlabs-stdlib' 5 | concat: 6 | repo: 'git://github.com/puppetlabs/puppetlabs-concat' 7 | ref: '2.1.0' 8 | vcsrepo: 9 | repo: 'git://github.com/puppetlabs/puppetlabs-vcsrepo' 10 | openssl: 11 | repo: 'git://github.com/camptocamp/puppet-openssl' 12 | symlinks: 13 | 'letsencrypt': "#{source_dir}" 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | Gemfile.lock 3 | vendor/ 4 | spec/fixtures/ 5 | .vagrant/ 6 | .bundle/ 7 | coverage/ 8 | log/ 9 | .idea/ 10 | *.iml 11 | *.swp 12 | .yardoc/* 13 | doc/* 14 | -------------------------------------------------------------------------------- /.msync.yml: -------------------------------------------------------------------------------- 1 | modulesync_config_version: '0.16.3' 2 | -------------------------------------------------------------------------------- /.nodeset.yml: -------------------------------------------------------------------------------- 1 | --- 2 | default_set: 'ubuntu-server-12042-x64' 3 | sets: 4 | 'centos-64-x64': 5 | nodes: 6 | "main.foo.vm": 7 | prefab: 'centos-64-x64' 8 | 'ubuntu-server-12042-x64': 9 | nodes: 10 | "main.foo.vm": 11 | prefab: 'ubuntu-server-12042-x64' 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-rspec 2 | AllCops: 3 | TargetRubyVersion: 1.9 4 | Include: 5 | - ./**/*.rb 6 | Exclude: 7 | - files/**/* 8 | - vendor/**/* 9 | - .vendor/**/* 10 | - pkg/**/* 11 | - spec/fixtures/**/* 12 | Lint/ConditionPosition: 13 | Enabled: True 14 | 15 | Lint/ElseLayout: 16 | Enabled: True 17 | 18 | Lint/UnreachableCode: 19 | Enabled: True 20 | 21 | Lint/UselessComparison: 22 | Enabled: True 23 | 24 | Lint/EnsureReturn: 25 | Enabled: True 26 | 27 | Lint/HandleExceptions: 28 | Enabled: True 29 | 30 | Lint/LiteralInCondition: 31 | Enabled: True 32 | 33 | Lint/ShadowingOuterLocalVariable: 34 | Enabled: True 35 | 36 | Lint/LiteralInInterpolation: 37 | Enabled: True 38 | 39 | Style/HashSyntax: 40 | Enabled: True 41 | 42 | Style/RedundantReturn: 43 | Enabled: True 44 | 45 | Lint/AmbiguousOperator: 46 | Enabled: True 47 | 48 | Lint/AssignmentInCondition: 49 | Enabled: True 50 | 51 | Style/SpaceBeforeComment: 52 | Enabled: True 53 | 54 | Style/AndOr: 55 | Enabled: True 56 | 57 | Style/RedundantSelf: 58 | Enabled: True 59 | 60 | Metrics/BlockLength: 61 | Enabled: False 62 | 63 | # Method length is not necessarily an indicator of code quality 64 | Metrics/MethodLength: 65 | Enabled: False 66 | 67 | # Module length is not necessarily an indicator of code quality 68 | Metrics/ModuleLength: 69 | Enabled: False 70 | 71 | Style/WhileUntilModifier: 72 | Enabled: True 73 | 74 | Lint/AmbiguousRegexpLiteral: 75 | Enabled: True 76 | 77 | Lint/Eval: 78 | Enabled: True 79 | 80 | Lint/BlockAlignment: 81 | Enabled: True 82 | 83 | Lint/DefEndAlignment: 84 | Enabled: True 85 | 86 | Lint/EndAlignment: 87 | Enabled: True 88 | 89 | Lint/DeprecatedClassMethods: 90 | Enabled: True 91 | 92 | Lint/Loop: 93 | Enabled: True 94 | 95 | Lint/ParenthesesAsGroupedExpression: 96 | Enabled: True 97 | 98 | Lint/RescueException: 99 | Enabled: True 100 | 101 | Lint/StringConversionInInterpolation: 102 | Enabled: True 103 | 104 | Lint/UnusedBlockArgument: 105 | Enabled: True 106 | 107 | Lint/UnusedMethodArgument: 108 | Enabled: True 109 | 110 | Lint/UselessAccessModifier: 111 | Enabled: True 112 | 113 | Lint/UselessAssignment: 114 | Enabled: True 115 | 116 | Lint/Void: 117 | Enabled: True 118 | 119 | Style/AccessModifierIndentation: 120 | Enabled: True 121 | 122 | Style/AccessorMethodName: 123 | Enabled: True 124 | 125 | Style/Alias: 126 | Enabled: True 127 | 128 | Style/AlignArray: 129 | Enabled: True 130 | 131 | Style/AlignHash: 132 | Enabled: True 133 | 134 | Style/AlignParameters: 135 | Enabled: True 136 | 137 | Metrics/BlockNesting: 138 | Enabled: True 139 | 140 | Style/AsciiComments: 141 | Enabled: True 142 | 143 | Style/Attr: 144 | Enabled: True 145 | 146 | Style/BracesAroundHashParameters: 147 | Enabled: True 148 | 149 | Style/CaseEquality: 150 | Enabled: True 151 | 152 | Style/CaseIndentation: 153 | Enabled: True 154 | 155 | Style/CharacterLiteral: 156 | Enabled: True 157 | 158 | Style/ClassAndModuleCamelCase: 159 | Enabled: True 160 | 161 | Style/ClassAndModuleChildren: 162 | Enabled: False 163 | 164 | Style/ClassCheck: 165 | Enabled: True 166 | 167 | # Class length is not necessarily an indicator of code quality 168 | Metrics/ClassLength: 169 | Enabled: False 170 | 171 | Style/ClassMethods: 172 | Enabled: True 173 | 174 | Style/ClassVars: 175 | Enabled: True 176 | 177 | Style/WhenThen: 178 | Enabled: True 179 | 180 | Style/WordArray: 181 | Enabled: True 182 | 183 | Style/UnneededPercentQ: 184 | Enabled: True 185 | 186 | Style/Tab: 187 | Enabled: True 188 | 189 | Style/SpaceBeforeSemicolon: 190 | Enabled: True 191 | 192 | Style/TrailingBlankLines: 193 | Enabled: True 194 | 195 | Style/SpaceInsideBlockBraces: 196 | Enabled: True 197 | 198 | Style/SpaceInsideBrackets: 199 | Enabled: True 200 | 201 | Style/SpaceInsideHashLiteralBraces: 202 | Enabled: True 203 | 204 | Style/SpaceInsideParens: 205 | Enabled: True 206 | 207 | Style/LeadingCommentSpace: 208 | Enabled: True 209 | 210 | Style/SpaceBeforeFirstArg: 211 | Enabled: True 212 | 213 | Style/SpaceAfterColon: 214 | Enabled: True 215 | 216 | Style/SpaceAfterComma: 217 | Enabled: True 218 | 219 | Style/SpaceAfterMethodName: 220 | Enabled: True 221 | 222 | Style/SpaceAfterNot: 223 | Enabled: True 224 | 225 | Style/SpaceAfterSemicolon: 226 | Enabled: True 227 | 228 | Style/SpaceAroundEqualsInParameterDefault: 229 | Enabled: True 230 | 231 | Style/SpaceAroundOperators: 232 | Enabled: True 233 | 234 | Style/SpaceBeforeBlockBraces: 235 | Enabled: True 236 | 237 | Style/SpaceBeforeComma: 238 | Enabled: True 239 | 240 | Style/CollectionMethods: 241 | Enabled: True 242 | 243 | Style/CommentIndentation: 244 | Enabled: True 245 | 246 | Style/ColonMethodCall: 247 | Enabled: True 248 | 249 | Style/CommentAnnotation: 250 | Enabled: True 251 | 252 | # 'Complexity' is very relative 253 | Metrics/CyclomaticComplexity: 254 | Enabled: False 255 | 256 | Style/ConstantName: 257 | Enabled: True 258 | 259 | Style/Documentation: 260 | Enabled: False 261 | 262 | Style/DefWithParentheses: 263 | Enabled: True 264 | 265 | Style/PreferredHashMethods: 266 | Enabled: True 267 | 268 | Style/DotPosition: 269 | EnforcedStyle: trailing 270 | 271 | Style/DoubleNegation: 272 | Enabled: True 273 | 274 | Style/EachWithObject: 275 | Enabled: True 276 | 277 | Style/EmptyLineBetweenDefs: 278 | Enabled: True 279 | 280 | Style/IndentArray: 281 | Enabled: True 282 | 283 | Style/IndentHash: 284 | Enabled: True 285 | 286 | Style/IndentationConsistency: 287 | Enabled: True 288 | 289 | Style/IndentationWidth: 290 | Enabled: True 291 | 292 | Style/EmptyLines: 293 | Enabled: True 294 | 295 | Style/EmptyLinesAroundAccessModifier: 296 | Enabled: True 297 | 298 | Style/EmptyLiteral: 299 | Enabled: True 300 | 301 | # Configuration parameters: AllowURI, URISchemes. 302 | Metrics/LineLength: 303 | Enabled: False 304 | 305 | Style/MethodCallParentheses: 306 | Enabled: True 307 | 308 | Style/MethodDefParentheses: 309 | Enabled: True 310 | 311 | Style/LineEndConcatenation: 312 | Enabled: True 313 | 314 | Style/TrailingWhitespace: 315 | Enabled: True 316 | 317 | Style/StringLiterals: 318 | Enabled: True 319 | 320 | Style/TrailingCommaInArguments: 321 | Enabled: True 322 | 323 | Style/TrailingCommaInLiteral: 324 | Enabled: True 325 | 326 | Style/GlobalVars: 327 | Enabled: True 328 | 329 | Style/GuardClause: 330 | Enabled: True 331 | 332 | Style/IfUnlessModifier: 333 | Enabled: True 334 | 335 | Style/MultilineIfThen: 336 | Enabled: True 337 | 338 | Style/NegatedIf: 339 | Enabled: True 340 | 341 | Style/NegatedWhile: 342 | Enabled: True 343 | 344 | Style/Next: 345 | Enabled: True 346 | 347 | Style/SingleLineBlockParams: 348 | Enabled: True 349 | 350 | Style/SingleLineMethods: 351 | Enabled: True 352 | 353 | Style/SpecialGlobalVars: 354 | Enabled: True 355 | 356 | Style/TrivialAccessors: 357 | Enabled: True 358 | 359 | Style/UnlessElse: 360 | Enabled: True 361 | 362 | Style/VariableInterpolation: 363 | Enabled: True 364 | 365 | Style/VariableName: 366 | Enabled: True 367 | 368 | Style/WhileUntilDo: 369 | Enabled: True 370 | 371 | Style/EvenOdd: 372 | Enabled: True 373 | 374 | Style/FileName: 375 | Enabled: True 376 | 377 | Style/For: 378 | Enabled: True 379 | 380 | Style/Lambda: 381 | Enabled: True 382 | 383 | Style/MethodName: 384 | Enabled: True 385 | 386 | Style/MultilineTernaryOperator: 387 | Enabled: True 388 | 389 | Style/NestedTernaryOperator: 390 | Enabled: True 391 | 392 | Style/NilComparison: 393 | Enabled: True 394 | 395 | Style/FormatString: 396 | Enabled: True 397 | 398 | Style/MultilineBlockChain: 399 | Enabled: True 400 | 401 | Style/Semicolon: 402 | Enabled: True 403 | 404 | Style/SignalException: 405 | Enabled: True 406 | 407 | Style/NonNilCheck: 408 | Enabled: True 409 | 410 | Style/Not: 411 | Enabled: True 412 | 413 | Style/NumericLiterals: 414 | Enabled: True 415 | 416 | Style/OneLineConditional: 417 | Enabled: True 418 | 419 | Style/OpMethod: 420 | Enabled: True 421 | 422 | Style/ParenthesesAroundCondition: 423 | Enabled: True 424 | 425 | Style/PercentLiteralDelimiters: 426 | Enabled: True 427 | 428 | Style/PerlBackrefs: 429 | Enabled: True 430 | 431 | Style/PredicateName: 432 | Enabled: True 433 | 434 | Style/RedundantException: 435 | Enabled: True 436 | 437 | Style/SelfAssignment: 438 | Enabled: True 439 | 440 | Style/Proc: 441 | Enabled: True 442 | 443 | Style/RaiseArgs: 444 | Enabled: True 445 | 446 | Style/RedundantBegin: 447 | Enabled: True 448 | 449 | Style/RescueModifier: 450 | Enabled: True 451 | 452 | # based on https://github.com/voxpupuli/modulesync_config/issues/168 453 | Style/RegexpLiteral: 454 | EnforcedStyle: percent_r 455 | Enabled: True 456 | 457 | Lint/UnderscorePrefixedVariableName: 458 | Enabled: True 459 | 460 | Metrics/ParameterLists: 461 | Enabled: False 462 | 463 | Lint/RequireParentheses: 464 | Enabled: True 465 | 466 | Style/SpaceBeforeFirstArg: 467 | Enabled: True 468 | 469 | Style/ModuleFunction: 470 | Enabled: True 471 | 472 | Lint/Debugger: 473 | Enabled: True 474 | 475 | Style/IfWithSemicolon: 476 | Enabled: True 477 | 478 | Style/Encoding: 479 | Enabled: True 480 | 481 | Style/BlockDelimiters: 482 | Enabled: True 483 | 484 | Style/MultilineBlockLayout: 485 | Enabled: True 486 | 487 | # 'Complexity' is very relative 488 | Metrics/AbcSize: 489 | Enabled: False 490 | 491 | # 'Complexity' is very relative 492 | Metrics/PerceivedComplexity: 493 | Enabled: False 494 | 495 | Lint/UselessAssignment: 496 | Enabled: True 497 | 498 | Style/ClosingParenthesisIndentation: 499 | Enabled: True 500 | 501 | # RSpec 502 | 503 | # We don't use rspec in this way 504 | RSpec/DescribeClass: 505 | Enabled: False 506 | 507 | # Example length is not necessarily an indicator of code quality 508 | RSpec/ExampleLength: 509 | Enabled: False 510 | 511 | RSpec/NamedSubject: 512 | Enabled: False 513 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: ruby 4 | cache: bundler 5 | bundler_args: --without system_tests development 6 | before_install: 7 | - bundle -v 8 | - rm Gemfile.lock || true 9 | - gem update --system 10 | - gem update bundler 11 | - gem --version 12 | - bundle -v 13 | script: 14 | - 'bundle exec rake $CHECK' 15 | matrix: 16 | fast_finish: true 17 | include: 18 | - rvm: 1.9.3 19 | env: PUPPET_VERSION="~> 3.0" STRICT_VARIABLES="yes" CHECK=test 20 | - rvm: 1.9.3 21 | env: PUPPET_VERSION="~> 3.0" STRICT_VARIABLES="yes" FUTURE_PARSER="yes" CHECK=test 22 | - rvm: 2.1.9 23 | env: PUPPET_VERSION="~> 3.0" STRICT_VARIABLES="yes" CHECK=test 24 | - rvm: 2.1.9 25 | env: PUPPET_VERSION="~> 4.0" CHECK=test 26 | - rvm: 2.2.5 27 | env: PUPPET_VERSION="~> 4.0" CHECK=test 28 | - rvm: 2.3.1 29 | env: PUPPET_VERSION="~> 4.0" CHECK=build DEPLOY_TO_FORGE=yes 30 | # - rvm: 2.3.1 31 | # env: PUPPET_VERSION="~> 4.0" CHECK=rubocop 32 | - rvm: 2.3.1 33 | env: PUPPET_VERSION="~> 4.0" CHECK=test 34 | - rvm: 2.4.0-preview1 35 | env: PUPPET_VERSION="~> 4.0" CHECK=test 36 | allow_failures: 37 | - rvm: 2.4.0-preview1 38 | notifications: 39 | email: false 40 | deploy: 41 | provider: puppetforge 42 | user: bzed 43 | password: 44 | secure: "ZInnboKjJg1ZN62Pb6t1tY2qPt0NEZWOlp+vo/CzY/Jxeoh8WdWVAVLyn6ay2/aPm0lgLYLNgHLEdB0qyZvgKXdajAjuZdVsrl5jAh/AoRTVpVTVx4YmpbFUxruO6DZcu3i5er3pKy5vuZjbdRhPBwObC4ImHJItS4FOt2aKb640s3nRk+AkM/pAoGOtpOzVLVDG/Wy/539MjQfvJ/qNlUBipSDkxshFRhq18rO/cg9yETCO1M7hG2qZgvqaX64jVhUs9nni+y9EMFdEGMIGULueALZ63aA9Bo/24q40mIDmMHF79eTNBhUCfugbXM5fYzwzVY+2NtEEgPmMY68/yCR0nN8fgWfenq69nUwS4/a2ThiCFswZftJRnCSqME2ZDPdhcwvo1cIDOKN3YPTn5bMbSG8wMt63o5viEU8hZoILNZUDpY+SkEUX1nIeNxWRr+paho3jX/PNHoaaZudt7ET57i7h+9Zt7UzhHRJ88yiL6LE6OACaluAH/9b+MhnquFd/h8zfL1AENuMnKyGmhXc1qoaCnSvzKb6qs65WSJq1gKY5dbrLCPlYnleMO4zb3p6o527RUrlX0fL8acas/QXc2RjiYw50kC4c+7MZcOtIteDiMfAoDbRi1cm9AHl/abh5eqEU5/4je3ePhVKa05TS2RgW3r493ykCQe3E8qk=" 45 | on: 46 | tags: true 47 | # all_branches is required to use tags 48 | all_branches: true 49 | # Only publish the build marked with "DEPLOY_TO_FORGE" 50 | condition: "$DEPLOY_TO_FORGE = yes" 51 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Release 0.1.17 2 | ###Summary 3 | 4 | - Last bugfix release before the module will be renamed to bzed-dehydrated. 5 | 6 | ####Bugfixes 7 | 8 | - Rubocop / travis-ci fixes 9 | - Inherit class params from params.pp 10 | 11 | ## Release 0.1.16 12 | ###Summary 13 | 14 | - letsencrypt.sh was renamed to dehydrated. This release makes bzed-letsencrypt work with dehydrated while staying compatible as much as possible. 15 | - I'll follow the rename - bzed-letsencrypt will be renamed to bzed-dehydrated before somebody tries to force me to do so. I understand that Let’s Encrypt™ needs to enforce their trademark, but they should find a better way to handle these issues. 16 | 17 | ####Features 18 | 19 | - Use 'dehydrated'. 20 | 21 | ####Bugfixes 22 | 23 | - Various little bugfixes 24 | 25 | 26 | ## Release 0.1.15 27 | ###Summary 28 | 29 | - Bugfix release 30 | - Better Puppetserver support 31 | 32 | ####Bugfixes 33 | 34 | - Various minor bugfixes, typo fixes. 35 | 36 | ####Improvements 37 | 38 | - Better variable validation 39 | - Use puppetserver variables automatically 40 | 41 | 42 | ## Release 0.1.14 43 | ###Summary 44 | 45 | - Introduce letsencrypt::certificate 46 | 47 | ####Improvements 48 | 49 | - With the new letsencrypt::certificate it is now possible to request certificates from other puppet modules. Subscribing to the define or receiving notifications from it is also possible. 50 | - Add an usage example to README.md, showing how to use letsencrypt::certificate with camptocamp-postfix. 51 | 52 | ## Release 0.1.13 53 | ###Summary 54 | 55 | - Bugfix-only release. 56 | 57 | ####Bugfixes 58 | 59 | - Don't require User['letsencrypt'] on clients - its only available on puppet masters. 60 | 61 | ## Release 0.1.12 62 | ###Summary 63 | 64 | - Avoid dh parameter generation on the puppet master host. 65 | 66 | ####Bugfixes 67 | 68 | - Add missing dependency on puppetlabs-vcsrepo. 69 | 70 | ####Improvements 71 | 72 | - Create dh files on the puppet client, not master. Made possible by requiring an uptodate concat version. 73 | 74 | ## Release 0.1.11 75 | ###Summary 76 | 77 | - Bugfix-only release. 78 | 79 | ####Bugfixes 80 | 81 | - Ensure we're compatible to older releases. 82 | - Remove some unused code. 83 | 84 | ## Release 0.1.10 85 | ###Summary 86 | 87 | - The 'OCSP' release. 88 | 89 | ####Features 90 | 91 | - Retrieve and ship OCSP stapling information into .crt.ocsp files. 92 | 93 | ####Bugfixes 94 | 95 | - Remove old crt chain / dh files. 96 | 97 | ####Improvements 98 | 99 | - Use file\_concat to build the full-key-chain file. 100 | 101 | 102 | ## Release 0.1.9 103 | ###Summary 104 | 105 | - The 'SAN-certificate' release. 106 | 107 | ####Features 108 | 109 | - Allow to use http/https proxies with letsencrypt.sh 110 | - Handle different letsencrypt CA URLs, useful for testing/debugging with 111 | the staging CA. 112 | - Passing a contact email for the letsencrypt registration is possible now, 113 | too. 114 | 115 | ####Bugfixes 116 | - Various minor bugs 117 | 118 | ####Improvements 119 | - Fix various issues in the openssl certificate config file. 120 | - Handle the private\_key.json file, keeping the registration 121 | information on disk. 122 | 123 | 124 | ## Release 0.1.8 125 | ###Summary 126 | 127 | - Generate full chain .pem file, including the key. 128 | 129 | ## Release 0.1.7 130 | ###Summary 131 | 132 | - Fixing typos / documentation. 133 | 134 | ## Release 0.1.6 135 | ###Summary 136 | 137 | - Create .dh files 138 | 139 | ## Release 0.1.5 140 | ###Summary 141 | 142 | - Fixing typos / documentation. 143 | 144 | ## Release 0.1.4 145 | ###Summary 146 | 147 | - Add basic travis.ci checks 148 | - Make rubocop and puppet-lint happy. 149 | 150 | ## Release 0.1.3 151 | ###Summary 152 | 153 | - Actually deploy the ca chain certificate 154 | - Add gitignore file. 155 | - Fixing typos / documentation. 156 | 157 | ## Release 0.1.2 158 | ###Summary 159 | 160 | - Fixing typos / documentation. 161 | - Handle empty certificate chain files 162 | - Various fixes 163 | 164 | ## Release 0.1.1 165 | ###Summary 166 | 167 | - First working release. 168 | 169 | 180 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source ENV['GEM_SOURCE'] || "https://rubygems.org" 2 | 3 | def location_for(place, fake_version = nil) 4 | if place =~ /^(git[:@][^#]*)#(.*)/ 5 | [fake_version, { :git => $1, :branch => $2, :require => false }].compact 6 | elsif place =~ /^file:\/\/(.*)/ 7 | ['>= 0', { :path => File.expand_path($1), :require => false }] 8 | else 9 | [place, { :require => false }] 10 | end 11 | end 12 | 13 | group :test do 14 | gem 'puppetlabs_spec_helper', '~> 1.2.2', :require => false 15 | gem 'rspec-puppet', '~> 2.5', :require => false 16 | gem 'rspec-puppet-facts', :require => false 17 | gem 'rspec-puppet-utils', :require => false 18 | gem 'puppet-lint-absolute_classname-check', :require => false 19 | gem 'puppet-lint-leading_zero-check', :require => false 20 | gem 'puppet-lint-trailing_comma-check', :require => false 21 | gem 'puppet-lint-version_comparison-check', :require => false 22 | gem 'puppet-lint-classes_and_types_beginning_with_digits-check', :require => false 23 | gem 'puppet-lint-unquoted_string-check', :require => false 24 | gem 'puppet-lint-variable_contains_upcase', :require => false 25 | gem 'metadata-json-lint', :require => false 26 | gem 'puppet-blacksmith', :require => false 27 | gem 'voxpupuli-release', :require => false, :git => 'https://github.com/voxpupuli/voxpupuli-release-gem.git' 28 | gem 'puppet-strings', '~> 0.99.0', :require => false 29 | gem 'rubocop-rspec', '~> 1.6', :require => false if RUBY_VERSION >= '2.3.0' 30 | gem 'json_pure', '<= 2.0.1', :require => false if RUBY_VERSION < '2.0.0' 31 | gem 'mocha', '>= 1.2.1', :require => false 32 | gem 'coveralls', :require => false if RUBY_VERSION >= '2.0.0' 33 | gem 'simplecov-console', :require => false if RUBY_VERSION >= '2.0.0' 34 | end 35 | 36 | group :development do 37 | gem 'travis', :require => false 38 | gem 'travis-lint', :require => false 39 | gem 'guard-rake', :require => false 40 | end 41 | 42 | group :system_tests do 43 | if beaker_version = ENV['BEAKER_VERSION'] 44 | gem 'beaker', *location_for(beaker_version) 45 | end 46 | if beaker_rspec_version = ENV['BEAKER_RSPEC_VERSION'] 47 | gem 'beaker-rspec', *location_for(beaker_rspec_version) 48 | else 49 | gem 'beaker-rspec', :require => false 50 | end 51 | gem 'serverspec', :require => false 52 | gem 'beaker-puppet_install_helper', :require => false 53 | end 54 | 55 | 56 | 57 | if facterversion = ENV['FACTER_GEM_VERSION'] 58 | gem 'facter', facterversion.to_s, :require => false, :groups => [:test] 59 | else 60 | gem 'facter', :require => false, :groups => [:test] 61 | end 62 | 63 | ENV['PUPPET_VERSION'].nil? ? puppetversion = '~> 4.0' : puppetversion = ENV['PUPPET_VERSION'].to_s 64 | gem 'puppet', puppetversion, :require => false, :groups => [:test] 65 | 66 | # vim: syntax=ruby 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED! 2 | **bzed-letsencrypt is deprecated! Please use bzed-dehydrated instead!** 3 | 4 | 5 | # letsencrypt 6 | 7 | [![Puppet Forge](http://img.shields.io/puppetforge/v/bzed/letsencrypt.svg)](https://forge.puppetlabs.com/bzed/letsencrypt) [![Build Status](https://travis-ci.org/bzed/bzed-letsencrypt.png?branch=master)](https://travis-ci.org/bzed/bzed-letsencrypt) 8 | 9 | 10 | #### Table of Contents 11 | 12 | 1. [Overview](#overview) 13 | 2. [Module Description - What the module does and why it is useful](#module-description) 14 | 3. [Setup - The basics of getting started with letsencrypt](#setup) 15 | * [What letsencrypt affects](#what-letsencrypt-affects) 16 | * [Setup requirements](#setup-requirements) 17 | * [Beginning with letsencrypt](#beginning-with-letsencrypt) 18 | 4. [Usage - Configuration options and additional functionality](#usage) 19 | 5. [Reference - An under-the-hood peek at what the module is doing and how](#reference) 20 | 5. [Limitations - OS compatibility, etc.](#limitations) 21 | 6. [Development - Guide for contributing to the module](#development) 22 | 23 | ## Overview 24 | 25 | Centralized CSR signing using Let’s Encrypt™ - keeping your keys safe on the host they belong to. 26 | 27 | ## Module Description 28 | 29 | bzed-letsencrypy creates private keys and CSRs, transfers 30 | the CSR to a puppetmaster where it is signed using 31 | the well known dehydrated 32 | https://github.com/lukas2511/dehydrated 33 | 34 | Signed certificates are shipped back to the appropriate host. 35 | 36 | You need to provide an appropriate hook script for letsencryt.sh, 37 | The default is to use the DNS-01 challenge, but if you hook 38 | supports it you could also create the necessary files for http-01. 39 | 40 | Let’s Encrypt is a trademark of the Internet Security Research Group. All rights reserved. 41 | 42 | ## Setup 43 | 44 | ### What letsencrypt affects 45 | 46 | 47 | * dehydrated is running at the puppetmaster host as it is easier 48 | to read and work with certificate files stored directly on the puppet 49 | master. Retrieving them using facter is unnecessarily complicated. 50 | 51 | 52 | ### Setup Requirements 53 | 54 | You need to ensure that exported ressources are working and pluginsync 55 | is enabled. 56 | 57 | ### Beginning with letsencrypt 58 | 59 | In the best case: add the letsencrypt class and override $domains 60 | with a list of domains you want to get certificates for. 61 | 62 | ## Usage 63 | ### On puppet nodes 64 | On a puppet node where you need your certificates: 65 | ~~~puppet 66 | class { 'letsencrypt' : 67 | domains => [ 'foo.example.com', 'fuzz.example.com' ], 68 | } 69 | ~~~ 70 | Key and CSR will be generated on your node and the CSR 71 | is shipped to your puppetmaster for signing - the puppetmaster needs 72 | a public interface and the cert is put on your node after some time. 73 | 74 | Additionally to or instead of specifying the domains as 75 | parameter to the letsencrypt class, it is possible to 76 | call the letsencrypt::certificate define directly: 77 | 78 | ~~~puppet 79 | ::letsencrypt::certificate { 'foo.example.com' : 80 | } 81 | ~~~ 82 | 83 | #### SAN Certificates 84 | Requesting SAN certificates is also possible. To do so pass a 85 | space seperated list of domainnames into the domains array. 86 | The first domainname in each list is used as the base domain 87 | for the request. For example: 88 | ~~~puppet 89 | class { 'letsencrypt' : 90 | domains => [ 91 | 'foo.example.com bar.example.com good.example.com', 92 | 'fuzz.example.com' 93 | ], 94 | } 95 | ~~~ 96 | 97 | And/or: 98 | ~~~puppet 99 | ::letsencrypt::certificate { 'foo.example.com bar.example.com good.example.com' : 100 | } 101 | ~~~ 102 | 103 | 104 | ### On your puppetmaster: 105 | What you need to prepare is a hook you want to use with dehydrated 106 | as you need to deploy the challenges somehow. Various examples for 107 | valid DNS-01 hooks are listed on 108 | https://github.com/lukas2511/dehydrated/wiki/Examples-for-DNS-01-hooks 109 | 110 | ~~~puppet 111 | class { 'letsencrypt' : 112 | hook_source => 'puppet:///modules/mymodule/dehydrated_hook' 113 | } 114 | ~~~ 115 | CSRs are collected and signed, and the resulting 116 | certificates and CA chain files are shipped back to your node. 117 | 118 | #### Testing and Debugging 119 | For testing purposes you want to use the staging CA, otherwise 120 | you'll hit rate limits pretty soon. To do s set the letsencrypt\_ca 121 | option: 122 | ~~~puppet 123 | class { 'letsencrypt' : 124 | hook_source => 'puppet:///modules/mymodule/dehydrated_hook', 125 | letsencrypt_ca => 'staging', 126 | } 127 | ~~~ 128 | 129 | ## Examples 130 | ### Postfix 131 | Using the _camptocamp-postfix_ module: 132 | 133 | ~~~puppet 134 | require ::letsencrypt::params 135 | $myhostname = $::fqdn 136 | 137 | $base_dir = $::letsencrypt::params::base_dir 138 | $crt_dir = $::letsencrypt::params::crt_dir 139 | $key_dir = $::letsencrypt::params::key_dir 140 | 141 | $postfix_chroot = '/var/spool/postfix' 142 | 143 | $tls_key = "${key_dir}/${myhostname}.key" 144 | $tls_cert = "${crt_dir}/${myhostname}_fullchain.pem" 145 | 146 | ::letsencrypt::certificate { $myhostname : 147 | notify => Service['postfix'], 148 | } 149 | 150 | ::postfix::config { 'smtpd_tls_cert_file' : 151 | value => $tls_cert, 152 | require => Letsencrypt::Certificate[$myhostname] 153 | } 154 | ::postfix::config { 'smtpd_tls_key_file' : 155 | value => $tls_key, 156 | require => Letsencrypt::Certificate[$myhostname] 157 | } 158 | ::postfix::config { 'smtpd_use_tls' : 159 | value => 'yes' 160 | } 161 | ::postfix::config { 'smtpd_tls_session_cache_database' : 162 | value => "btree:\${data_directory}/smtpd_scache", 163 | } 164 | ::postfix::config { 'smtp_tls_session_cache_database' : 165 | value => "btree:\${data_directory}/smtp_scache", 166 | } 167 | ::postfix::config { 'smtp_tls_security_level' : 168 | value => 'may', 169 | } 170 | 171 | file { [ 172 | "${postfix_chroot}/${base_dir}", 173 | "${postfix_chroot}/${crt_dir}", 174 | ] : 175 | ensure => directory, 176 | owner => 'root', 177 | group => 'root', 178 | mode => '0755', 179 | } 180 | 181 | file { "${postfix_chroot}/${key_dir}" : 182 | ensure => directory, 183 | owner => 'root', 184 | group => 'root', 185 | mode => '0750', 186 | } 187 | 188 | file { "${postfix_chroot}/${tls_key}" : 189 | ensure => file, 190 | owner => 'root', 191 | group => 'root', 192 | mode => '0640', 193 | source => $tls_key, 194 | subscribe => Letsencrypt::Certificate[$myhostname], 195 | notify => Service['postfix'], 196 | } 197 | 198 | file { "${postfix_chroot}/${tls_cert}" : 199 | ensure => file, 200 | owner => 'root', 201 | group => 'root', 202 | mode => '0644', 203 | source => $tls_cert, 204 | subscribe => Letsencrypt::Certificate[$myhostname], 205 | notify => Service['postfix'], 206 | } 207 | 208 | ~~~ 209 | 210 | ## Reference 211 | 212 | Classes: 213 | * letsencrypt 214 | * letsencrypt::params 215 | * letsencrypt::request::handler 216 | 217 | Defines: 218 | * letsencrypt::csr 219 | * letsencrypt::deploy 220 | * letsencrypt::deploy::crt 221 | * letsencrypt::request 222 | * letsencrypt::request::crt 223 | 224 | Facts: 225 | * letsencrypt\_csrs 226 | * letsencrypt\_csr\_\* 227 | * letsencrypt\_crts 228 | 229 | ## Limitations 230 | 231 | Not really well tested yet, documentation missing, no spec tests.... 232 | 233 | ## Development 234 | 235 | Patches are very welcome! 236 | Please send your pull requests on github! 237 | 238 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'puppetlabs_spec_helper/rake_tasks' 2 | require 'puppet_blacksmith/rake_tasks' 3 | require 'voxpupuli/release/rake_tasks' 4 | require 'puppet-strings/tasks' 5 | 6 | PuppetLint.configuration.log_format = '%{path}:%{line}:%{check}:%{KIND}:%{message}' 7 | PuppetLint.configuration.fail_on_warnings = true 8 | PuppetLint.configuration.send('relative') 9 | PuppetLint.configuration.send('disable_140chars') 10 | PuppetLint.configuration.send('disable_class_inherits_from_params_class') 11 | PuppetLint.configuration.send('disable_documentation') 12 | PuppetLint.configuration.send('disable_single_quote_string_with_variables') 13 | 14 | exclude_paths = %w( 15 | pkg/**/* 16 | vendor/**/* 17 | .vendor/**/* 18 | spec/**/* 19 | ) 20 | PuppetLint.configuration.ignore_paths = exclude_paths 21 | PuppetSyntax.exclude_paths = exclude_paths 22 | 23 | desc 'Run acceptance tests' 24 | RSpec::Core::RakeTask.new(:acceptance) do |t| 25 | t.pattern = 'spec/acceptance' 26 | end 27 | 28 | desc 'Run tests metadata_lint, release_checks' 29 | task test: [ 30 | :metadata_lint, 31 | :release_checks, 32 | ] 33 | # vim: syntax=ruby 34 | -------------------------------------------------------------------------------- /examples/init.pp: -------------------------------------------------------------------------------- 1 | # The baseline for module testing used by Puppet Labs is that each manifest 2 | # should have a corresponding test manifest that declares that class or defined 3 | # type. 4 | # 5 | # Tests are then run by using puppet apply --noop (to check for compilation 6 | # errors and view a log of events) or by fully applying the test in a virtual 7 | # environment (to compare the resulting system state to the desired state). 8 | # 9 | # Learn more about module testing here: 10 | # http://docs.puppetlabs.com/guides/tests_smoke.html 11 | # 12 | include ::letsencrypt 13 | -------------------------------------------------------------------------------- /files/letsencrypt_check_altnames.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 5 | 6 | CSR=${1} 7 | CRT=${2} 8 | 9 | declare -a CSR_DNS=($(openssl req -text -noout -in ${CSR} | awk '/DNS/ {print}' | sed s/,//g)) 10 | declare -a CRT_DNS=($(openssl x509 -text -noout -in ${CRT} -certopt no_subject,no_header,no_version,no_serial,no_signame,no_validity,no_subject,no_issuer,no_pubkey,no_sigdump,no_aux | awk '/DNS/ {print}' | sed s/,//g)) 11 | declare -a DIFF=() 12 | 13 | OLD_IFS=$IFS 14 | IFS=$'\n\t' 15 | 16 | DIFF=($(comm -3 <(echo "${CSR_DNS[*]}" | sort -u) <(echo "${CRT_DNS[*]}" | sort -u))) 17 | IFS=${OLD_IFS} 18 | 19 | test -z "${DIFF[*]}" || exit 1 20 | exit 0 21 | -------------------------------------------------------------------------------- /lib/facter/letsencrypyt_certs.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'facter' 3 | 4 | crt_domains = Dir['/etc/letsencrypt/certs/*.crt'].map { |a| a.gsub(%r{\.crt$}, '').gsub(%r{^.*/}, '') } 5 | 6 | Facter.add(:letsencrypt_certs) do 7 | setcode do 8 | crt_domains 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/facter/letsencrypyt_crt.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'facter' 3 | 4 | crt_domains = Dir['/opt/letsencrypt/requests/*/*.crt'].map { |a| a.gsub(%r{\.crt$}, '').gsub(%r{^.*/}, '') } 5 | 6 | Facter.add(:letsencrypt_crts) do 7 | setcode do 8 | crt_domains.join(',') if crt_domains 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/facter/letsencrypyt_csr.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'facter' 3 | 4 | csr_domains = Dir['/etc/letsencrypt/csr/*.csr'].map { |a| a.gsub(%r{\.csr$}, '').gsub(%r{^.*/}, '') } 5 | 6 | Facter.add(:letsencrypt_csrs) do 7 | setcode do 8 | csr_domains.join(',') if csr_domains 9 | end 10 | end 11 | 12 | csr_domains.each do |csr_domain| 13 | Facter.add('letsencrypt_csr_' + csr_domain) do 14 | setcode do 15 | csr = File.read("/etc/letsencrypt/csr/#{csr_domain}.csr") 16 | csr 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/puppet/parser/functions/file_or_empty_string.rb: -------------------------------------------------------------------------------- 1 | # Returns the contents of a file - or an empty string 2 | # if the file does not exist. based on file.rb from puppet. 3 | 4 | Puppet::Parser::Functions.newfunction( 5 | :file_or_empty_string, 6 | :type => :rvalue, 7 | :doc => <<-EOS 8 | Return the contents of a file. Multiple files 9 | can be passed, and the first file that exists will be read in. 10 | EOS 11 | ) do |vals| 12 | ret = nil 13 | vals.each do |file| 14 | unless Puppet::Util.absolute_path?(file) 15 | raise(Puppet::ParseError, 'Files must be fully qualified') 16 | end 17 | if FileTest.exists?(file) 18 | ret = File.read(file) 19 | break 20 | end 21 | end 22 | if ret 23 | ret 24 | else 25 | '' 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /manifests/certificate.pp: -------------------------------------------------------------------------------- 1 | # == Define: letsencrypt::certificate 2 | # 3 | # Request a certificate for a single domain or a SAN certificate. 4 | # 5 | # === Parameters 6 | # 7 | # [*domain*] 8 | # Full qualified domain names (== commonname) 9 | # you want to request a certificate for. 10 | # For SAN certificates you need to pass space seperated strings, 11 | # for example 'foo.example.com fuzz.example.com' 12 | # 13 | # [*channlengetype*] 14 | # Challenge type to use, defaults to $::letsencrypt::challengetype 15 | # 16 | # [*letsencrypt_host*] 17 | # The host you want to run dehydrated on. 18 | # Defaults to $::letsencrypt::letsencrypt_host 19 | # 20 | # [*dh_param_size*] 21 | # dh parameter size, defaults to $::letsencrypt::dh_param_size 22 | # 23 | # === Examples 24 | # ::letsencryt::certificate( 'foo.example.com' : 25 | # } 26 | # 27 | # === Authors 28 | # 29 | # Author Name Bernd Zeimetz 30 | # 31 | # === Copyright 32 | # 33 | # Copyright 2016 Bernd Zeimetz 34 | # 35 | define letsencrypt::certificate ( 36 | $domain = $name, 37 | $challengetype = $::letsencrypt::challengetype, 38 | $letsencrypt_host = $::letsencrypt::letsencrypt_host, 39 | $dh_param_size = $::letsencrypt::dh_param_size, 40 | ){ 41 | 42 | validate_integer($dh_param_size) 43 | validate_string($letsencrypt_host) 44 | validate_re($challengetype, '^(http-01|dns-01)$') 45 | validate_string($domain) 46 | 47 | require ::letsencrypt::params 48 | require ::letsencrypt::setup 49 | 50 | ::letsencrypt::deploy { $domain : 51 | letsencrypt_host => $letsencrypt_host, 52 | } 53 | ::letsencrypt::csr { $domain : 54 | letsencrypt_host => $letsencrypt_host, 55 | challengetype => $challengetype, 56 | dh_param_size => $dh_param_size, 57 | } 58 | 59 | deprecation( 60 | 'bzed-letsencrypt-deprecated', 61 | 'bzed-letsencrypt is deprecated, please use bzed-dehydrated instead!' 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /manifests/csr.pp: -------------------------------------------------------------------------------- 1 | # = Define: letsencrypt::csr 2 | # 3 | # Create a CSR and ask to sign it. 4 | # 5 | # == Parameters: 6 | # 7 | # [*letsencrypt_host*] 8 | # Host the certificates will be signed on 9 | # 10 | # [*challengetype*] 11 | # challengetype dehydratedould use. 12 | # 13 | # .... plus various other undocumented parameters 14 | # 15 | # 16 | # === Authors 17 | # 18 | # Author Name Bernd Zeimetz 19 | # 20 | # === Copyright 21 | # 22 | # Copyright 2016 Bernd Zeimetz 23 | # 24 | 25 | define letsencrypt::csr( 26 | $letsencrypt_host, 27 | $challengetype, 28 | $domain_list = $name, 29 | $country = undef, 30 | $state = undef, 31 | $locality = undef, 32 | $organization = undef, 33 | $unit = undef, 34 | $email = undef, 35 | $password = undef, 36 | $ensure = 'present', 37 | $force = true, 38 | $dh_param_size = 2048, 39 | ) { 40 | require ::letsencrypt::params 41 | 42 | validate_string($letsencrypt_host) 43 | validate_string($country) 44 | validate_string($organization) 45 | validate_string($domain_list) 46 | validate_string($ensure) 47 | validate_string($state) 48 | validate_string($locality) 49 | validate_string($unit) 50 | validate_string($email) 51 | validate_integer($dh_param_size) 52 | 53 | $base_dir = $::letsencrypt::params::base_dir 54 | $csr_dir = $::letsencrypt::params::csr_dir 55 | $key_dir = $::letsencrypt::params::key_dir 56 | $crt_dir = $::letsencrypt::params::crt_dir 57 | 58 | $domains = split($domain_list, ' ') 59 | $domain = $domains[0] 60 | if (size(domains) > 1) { 61 | $req_ext = true 62 | $altnames = delete_at($domains, 0) 63 | $subject_alt_names = $domains 64 | } else { 65 | $req_ext = false 66 | $altnames = [] 67 | $subject_alt_names = [] 68 | } 69 | 70 | $cnf = "${base_dir}/${domain}.cnf" 71 | $crt = "${crt_dir}/${domain}.crt" 72 | $key = "${key_dir}/${domain}.key" 73 | $csr = "${csr_dir}/${domain}.csr" 74 | $dh = "${crt_dir}/${domain}.dh" 75 | 76 | $create_dh_unless = join([ 77 | '/usr/bin/test', 78 | '-f', 79 | "'${dh}'", 80 | '&&', 81 | '/usr/bin/test', 82 | '$(', 83 | "/usr/bin/stat -c '%Y' ${dh}", 84 | ')', 85 | '-gt', 86 | '$(', 87 | "/bin/date --date='1 month ago' '+%s'", 88 | ')', 89 | ], ' ') 90 | 91 | exec { "create-dh-${dh}" : 92 | require => [ 93 | File[$crt_dir] 94 | ], 95 | user => 'root', 96 | group => $::letsencrypt::group, 97 | command => "/usr/bin/openssl dhparam -check ${dh_param_size} > ${dh}", 98 | unless => $create_dh_unless, 99 | timeout => 30*60, 100 | } 101 | 102 | file { $dh : 103 | ensure => $ensure, 104 | owner => 'root', 105 | group => $::letsencrypt::group, 106 | mode => '0644', 107 | require => Exec["create-dh-${dh}"], 108 | } 109 | 110 | file { $cnf : 111 | ensure => $ensure, 112 | owner => 'root', 113 | group => $::letsencrypt::group, 114 | mode => '0644', 115 | content => template('letsencrypt/cert.cnf.erb'), 116 | } 117 | 118 | ssl_pkey { $key : 119 | ensure => $ensure, 120 | password => $password, 121 | require => File[$key_dir], 122 | } 123 | x509_request { $csr : 124 | ensure => $ensure, 125 | template => $cnf, 126 | private_key => $key, 127 | password => $password, 128 | force => $force, 129 | require => File[$cnf], 130 | } 131 | 132 | exec { "refresh-csr-${csr}" : 133 | path => '/sbin:/bin:/usr/sbin:/usr/bin', 134 | command => "rm -f ${csr}", 135 | refreshonly => true, 136 | user => 'root', 137 | group => $::letsencrypt::group, 138 | before => X509_request[$csr], 139 | subscribe => File[$cnf], 140 | } 141 | 142 | file { $key : 143 | ensure => $ensure, 144 | owner => 'root', 145 | group => $::letsencrypt::group, 146 | mode => '0640', 147 | require => Ssl_pkey[$key], 148 | } 149 | file { $csr : 150 | ensure => $ensure, 151 | owner => 'root', 152 | group => $::letsencrypt::group, 153 | mode => '0644', 154 | require => X509_request[$csr], 155 | } 156 | 157 | $csr_content = pick_default(getvar("::letsencrypt_csr_${domain}"), getvar("facts.'letsencrypt_csr_${domain}'"), '') 158 | if ($csr_content =~ /CERTIFICATE REQUEST/) { 159 | @@letsencrypt::request { $domain : 160 | csr => $csr_content, 161 | tag => "crt-host-${letsencrypt_host}", 162 | challengetype => $challengetype, 163 | altnames => $altnames, 164 | } 165 | } else { 166 | notify { "no CSR from facter for domain ${domain}" : } 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /manifests/deploy.pp: -------------------------------------------------------------------------------- 1 | # = Define: letsencrypt::deploy 2 | # 3 | # Collects signed certificates and installs them. 4 | # 5 | # == Parameters: 6 | # 7 | # [*letsencrypt_host*] 8 | # Host the certificates were signed on 9 | # 10 | # === Authors 11 | # 12 | # Author Name Bernd Zeimetz 13 | # 14 | # === Copyright 15 | # 16 | # Copyright 2016 Bernd Zeimetz 17 | # 18 | 19 | 20 | define letsencrypt::deploy( 21 | $letsencrypt_host, 22 | ) { 23 | 24 | $domains = split($name, ' ') 25 | $domain = $domains[0] 26 | 27 | Letsencrypt::Deploy::Crt <<| tag == $domain and tag == $letsencrypt_host |>> 28 | 29 | } 30 | -------------------------------------------------------------------------------- /manifests/deploy/crt.pp: -------------------------------------------------------------------------------- 1 | # = Define: letsencrypt::crt 2 | # 3 | # Used as exported ressource to ship a signed CRT. 4 | # 5 | # == Parameters: 6 | # 7 | # [*crt_content*] 8 | # actual certificate content. 9 | # 10 | # [*crt_chain_content*] 11 | # actual certificate chain file content. 12 | # 13 | # [*domain*] 14 | # Certificate commonname / domainname. 15 | # 16 | # 17 | # === Authors 18 | # 19 | # Author Name Bernd Zeimetz 20 | # 21 | # === Copyright 22 | # 23 | # Copyright 2016 Bernd Zeimetz 24 | # 25 | 26 | 27 | define letsencrypt::deploy::crt( 28 | $crt_content, 29 | $crt_chain_content, 30 | $ocsp_content, 31 | $domain = $name 32 | ) { 33 | 34 | require ::letsencrypt::params 35 | 36 | $crt_dir = $::letsencrypt::params::crt_dir 37 | $key_dir = $::letsencrypt::params::key_dir 38 | $crt = "${crt_dir}/${domain}.crt" 39 | $ocsp = "${crt_dir}/${domain}.crt.ocsp" 40 | $key = "${key_dir}/${domain}.key" 41 | $dh = "${crt_dir}/${domain}.dh" 42 | $crt_chain = "${crt_dir}/${domain}_ca.pem" 43 | $crt_full_chain = "${crt_dir}/${domain}_fullchain.pem" 44 | $crt_full_chain_with_key = "${key_dir}/${domain}_fullchain_with_key.pem" 45 | 46 | file { $crt : 47 | ensure => file, 48 | owner => root, 49 | group => $::letsencrypt::group, 50 | content => $crt_content, 51 | mode => '0644', 52 | } 53 | 54 | if !empty($ocsp_content) { 55 | file { $ocsp : 56 | ensure => file, 57 | owner => root, 58 | group => $::letsencrypt::group, 59 | content => base64('decode', $ocsp_content), 60 | mode => '0644', 61 | } 62 | } else { 63 | file { $ocsp : 64 | ensure => absent, 65 | force => true, 66 | } 67 | } 68 | 69 | concat { $crt_full_chain : 70 | owner => root, 71 | group => $::letsencrypt::group, 72 | mode => '0644', 73 | } 74 | concat { $crt_full_chain_with_key : 75 | owner => root, 76 | group => $::letsencrypt::group, 77 | mode => '0640', 78 | } 79 | 80 | concat::fragment { "${domain}_key" : 81 | target => $crt_full_chain_with_key, 82 | source => $key, 83 | order => '01', 84 | } 85 | concat::fragment { "${domain}_fullchain" : 86 | target => $crt_full_chain_with_key, 87 | source => $crt_full_chain, 88 | order => '10', 89 | subscribe => Concat[$crt_full_chain], 90 | } 91 | 92 | concat::fragment { "${domain}_crt" : 93 | target => $crt_full_chain, 94 | content => $crt_content, 95 | order => '10', 96 | } 97 | concat::fragment { "${domain}_dh" : 98 | target => $crt_full_chain, 99 | source => $dh, 100 | order => '30', 101 | require => File[$dh], 102 | } 103 | 104 | if ($crt_chain_content and $crt_chain_content =~ /BEGIN CERTIFICATE/) { 105 | file { $crt_chain : 106 | ensure => file, 107 | owner => root, 108 | group => $::letsencrypt::group, 109 | content => $crt_chain_content, 110 | mode => '0644', 111 | } 112 | concat::fragment { "${domain}_ca" : 113 | target => $crt_full_chain, 114 | content => $crt_chain_content, 115 | order => '50', 116 | } 117 | } else { 118 | file { $crt_chain : 119 | ensure => absent, 120 | force => true, 121 | } 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /manifests/init.pp: -------------------------------------------------------------------------------- 1 | # == Class: letsencrypt 2 | # 3 | # Include this class if you would like to create 4 | # Certificates or on your puppetmaster to have you CSRs signed. 5 | # 6 | # 7 | # === Parameters 8 | # 9 | # [*domains*] 10 | # Array of full qualified domain names (== commonname) 11 | # you want to request a certificate for. 12 | # For SAN certificates you need to pass space seperated strings, 13 | # for example ['foo.example.com fuzz.example.com', 'blub.example.com'] 14 | # 15 | # [*dehydrated_git_url*] 16 | # URL used to checkout the dehydrated using git. 17 | # Defaults to the upstream github url. 18 | # 19 | # [*channlengetype*] 20 | # Challenge type to use, default is 'dns-01'. Your dehydrated 21 | # hook needs to be able to handle it. 22 | # 23 | # [*hook_source*] 24 | # Points to the source of the dehydrated hook you'd like to 25 | # distribute ((as in file { ...: source => }) 26 | # hook_source or hook_content needs to be specified. 27 | # 28 | # [*hook_content*] 29 | # The actual content (as in file { ...: content => }) of the 30 | # dehydrated hook. 31 | # hook_source or hook_content needs to be specified. 32 | # 33 | # [*domain_validation_hook_source*] 34 | # Source of a domain validation hook which runs before 35 | # dehydrated. If the hook fails, dehydrated won't be executed. 36 | # Can be used to check if the real hook will actually succeed - 37 | # usable to avoid being blocked form letsencrypt. 38 | # This setting is optional. 39 | # 40 | # [*domain_validation_hook_content*] 41 | # The actual content (as in file { ...: content => }) of the 42 | # domain_validation_hook. See *domain_validation_hook_source* 43 | # for a description. 44 | # 45 | # [*hook_env*] 46 | # Additional environment variables to set when calling the 47 | # verification hook. For example, credentials can be passed 48 | # this way. This setting is optional. 49 | # 50 | # [*letsencrypt_host*] 51 | # The host you want to run dehydrated on. 52 | # For now it needs to be a puppetmaster, as it needs direct access 53 | # to the certificates using functions in puppet. 54 | # 55 | # [*letsencrypt_ca*] 56 | # The letsencrypt CA you want to use. For debugging you want to 57 | # set it to 'https://acme-staging.api.letsencrypt.org/directory' 58 | # 59 | # [*letsencrypt_contact_email*] 60 | # E-mail to use during the letsencrypt account registration. 61 | # If undef, no email address is being used. 62 | # 63 | # [*letsencrypt_proxy*] 64 | # Proxyserver to use to connect to the letsencrypt CA 65 | # for example '127.0.0.1:3128' 66 | # 67 | # [*dh_param_size*] 68 | # dh parameter size, defaults to 2048 69 | # 70 | # [*manage_packages*] 71 | # install necessary packages, mainly git 72 | # 73 | # === Examples 74 | # class { 'letsencrypt' : 75 | # domains => [ 'foo.example.com', 'fuzz.example.com' ], 76 | # hook_source => 'puppet:///modules/mymodule/dehydrated_hook' 77 | # } 78 | # 79 | # === Authors 80 | # 81 | # Author Name Bernd Zeimetz 82 | # 83 | # === Copyright 84 | # 85 | # Copyright 2016 Bernd Zeimetz 86 | # 87 | class letsencrypt ( 88 | $domains = [], 89 | $letsencrypt_sh_git_url = $::letsencrypt::params::letsencrypt_sh_git_url, 90 | $dehydrated_git_url = $letsencrypt_sh_git_url, 91 | $challengetype = $::letsencrypt::params::challengetype, 92 | $domain_validation_hook_source = undef, 93 | $domain_validation_hook_content = undef, 94 | $hook_source = undef, 95 | $hook_content = undef, 96 | $hook_env = $::letsencrypt::params::dehydrated_hook_env, 97 | $letsencrypt_host = $::letsencrypt::params::letsencrypt_host, 98 | $letsencrypt_ca = $::letsencrypt::params::letsencrypt_ca, 99 | $letsencrypt_cas = $::letsencrypt::params::letsencrypt_cas, 100 | $letsencrypt_contact_email = undef, 101 | $letsencrypt_proxy = undef, 102 | $dh_param_size = $::letsencrypt::params::dh_param_size, 103 | $manage_packages = $::letsencrypt::params::manage_packages, 104 | $manage_user = $::letsencrypt::params::manage_user, 105 | $user = $::letsencrypt::params::user, 106 | $group = $::letsencrypt::params::group, 107 | ) inherits ::letsencrypt::params { 108 | 109 | require ::letsencrypt::setup 110 | 111 | if ($::fqdn == $letsencrypt_host) { 112 | class { '::letsencrypt::setup::puppetmaster' : 113 | manage_packages => $manage_packages, 114 | } 115 | 116 | if !($hook_source or $hook_content) { 117 | notify { '$hook_source or $hook_content needs to be specified!' : 118 | loglevel => err, 119 | } 120 | } else { 121 | class { '::letsencrypt::request::handler' : 122 | dehydrated_git_url => $dehydrated_git_url, 123 | letsencrypt_ca => $letsencrypt_ca, 124 | letsencrypt_cas => $letsencrypt_cas, 125 | hook_source => $hook_source, 126 | hook_content => $hook_content, 127 | domain_validation_hook_source => $domain_validation_hook_source, 128 | domain_validation_hook_content => $domain_validation_hook_content, 129 | letsencrypt_contact_email => $letsencrypt_contact_email, 130 | letsencrypt_proxy => $letsencrypt_proxy, 131 | } 132 | } 133 | if ($::letsencrypt_crts and $::letsencrypt_crts != '') { 134 | $letsencrypt_crts_array = split($::letsencrypt_crts, ',') 135 | ::letsencrypt::request::crt { $letsencrypt_crts_array : } 136 | } 137 | } 138 | 139 | ::letsencrypt::certificate { $domains : 140 | letsencrypt_host => $letsencrypt_host, 141 | challengetype => $challengetype, 142 | dh_param_size => $dh_param_size, 143 | } 144 | 145 | deprecation( 146 | 'bzed-letsencrypt-deprecated', 147 | 'bzed-letsencrypt is deprecated, please use bzed-dehydrated instead!' 148 | ) 149 | } 150 | -------------------------------------------------------------------------------- /manifests/params.pp: -------------------------------------------------------------------------------- 1 | # == Class: letsencrypt::params 2 | # 3 | # Some basic variables we want to use. 4 | # 5 | # === Authors 6 | # 7 | # Author Name Bernd Zeimetz 8 | # 9 | # === Copyright 10 | # 11 | # Copyright 2016 Bernd Zeimetz 12 | # 13 | 14 | 15 | 16 | class letsencrypt::params { 17 | 18 | $base_dir = '/etc/letsencrypt' 19 | $csr_dir = "${base_dir}/csr" 20 | $key_dir = "${base_dir}/private" 21 | $crt_dir = "${base_dir}/certs" 22 | 23 | $handler_base_dir = '/opt/letsencrypt' 24 | $handler_requests_dir = "${handler_base_dir}/requests" 25 | 26 | $dehydrated_dir = "${handler_base_dir}/dehydrated" 27 | $dehydrated_hook = "${handler_base_dir}/letsencrypt_hook" 28 | $dehydrated_hook_env = [] 29 | $domain_validation_hook = "${handler_base_dir}/domain_validation_hook" 30 | $dehydrated_conf = "${handler_base_dir}/letsencrypt.conf" 31 | $dehydrated = "${dehydrated_dir}/dehydrated" 32 | 33 | $letsencrypt_chain_request = "${handler_base_dir}/letsencrypt_get_certificate_chain.sh" 34 | $letsencrypt_ocsp_request = "${handler_base_dir}/letsencrypt_get_certificate_ocsp.sh" 35 | $letsencrypt_check_altnames = "${handler_base_dir}/letsencrypt_check_altnames.sh" 36 | 37 | if defined('$puppetmaster') { 38 | $letsencrypt_host = $::puppetmaster 39 | } elsif defined('$servername') { 40 | $letsencrypt_host = $::servername 41 | } 42 | 43 | $letsencrypt_sh_git_url = 'https://github.com/lukas2511/dehydrated.git' 44 | $dehydrated_git_url = $letsencrypt_sh_git_url 45 | $challengetype = 'dns-01' 46 | $letsencrypt_ca = 'production' 47 | $letsencrypt_cas = { 48 | 'production' => { 49 | 'url' => 'https://acme-v01.api.letsencrypt.org/directory', 50 | 'hash' => 'aHR0cHM6Ly9hY21lLXYwMS5hcGkubGV0c2VuY3J5cHQub3JnL2RpcmVjdG9yeQo' 51 | }, 52 | 'staging' => { 53 | 'url' => 'https://acme-staging.api.letsencrypt.org/directory', 54 | 'hash' => 'aHR0cHM6Ly9hY21lLXN0YWdpbmcuYXBpLmxldHNlbmNyeXB0Lm9yZy9kaXJlY3RvcnkK' 55 | }, 56 | 'v2-production' => { 57 | 'url' => 'https://acme-v02.api.letsencrypt.org/directory', 58 | 'hash' => 'aHR0cHM6Ly9hY21lLXYwMi5hcGkubGV0c2VuY3J5cHQub3JnL2RpcmVjdG9yeQo' 59 | }, 60 | 'v2-staging' => { 61 | 'url' => 'https://acme-staging-v02.api.letsencrypt.org/directory', 62 | 'hash' => 'aHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvZGlyZWN0b3J5Cg' 63 | }, 64 | } 65 | $dh_param_size = 2048 66 | $manage_packages = true 67 | $manage_user = true 68 | $user = 'letsencrypt' 69 | $group = 'letsencrypt' 70 | } 71 | -------------------------------------------------------------------------------- /manifests/request.pp: -------------------------------------------------------------------------------- 1 | # = Define: letsencrypt::request 2 | # 3 | # Request to sign a CSR. 4 | # 5 | # == Parameters: 6 | # 7 | # [*csr*] 8 | # The full csr as string. 9 | # 10 | # [*domain*] 11 | # Certificate commonname / domainname. 12 | # 13 | # [*challengetype*] 14 | # challengetype dehydratedould use. 15 | # 16 | # 17 | # === Authors 18 | # 19 | # Author Name Bernd Zeimetz 20 | # 21 | # === Copyright 22 | # 23 | # Copyright 2016 Bernd Zeimetz 24 | # 25 | 26 | 27 | define letsencrypt::request ( 28 | $csr, 29 | $challengetype, 30 | $domain = $name, 31 | $altnames = undef, 32 | ) { 33 | 34 | require ::letsencrypt::params 35 | 36 | $handler_requests_dir = $::letsencrypt::params::handler_requests_dir 37 | 38 | $base_dir = "${handler_requests_dir}/${domain}" 39 | $csr_file = "${base_dir}/${domain}.csr" 40 | $crt_file = "${base_dir}/${domain}.crt" 41 | $crt_chain_file = "${base_dir}/${domain}_ca.pem" 42 | $dehydrated = $::letsencrypt::params::dehydrated 43 | $dehydrated_dir = $::letsencrypt::params::dehydrated_dir 44 | $dehydrated_hook = $::letsencrypt::params::dehydrated_hook 45 | $dehydrated_hook_env = $::letsencrypt::hook_env 46 | $dehydrated_conf = $::letsencrypt::params::dehydrated_conf 47 | $letsencrypt_chain_request = $::letsencrypt::params::letsencrypt_chain_request 48 | $letsencrypt_check_altnames = $::letsencrypt::params::letsencrypt_check_altnames 49 | $domain_validation_hook = $::letsencrypt::params::domain_validation_hook 50 | 51 | File { 52 | owner => $::letsencrypt::user, 53 | group => $::letsencrypt::group, 54 | require => [ 55 | User[$::letsencrypt::user], 56 | Group[$::letsencrypt::group] 57 | ], 58 | } 59 | 60 | file { $base_dir : 61 | ensure => directory, 62 | mode => '0755', 63 | } 64 | 65 | file { $csr_file : 66 | ensure => file, 67 | content => $csr, 68 | mode => '0640', 69 | } 70 | 71 | $_csr_file = shellquote($csr_file) 72 | $_crt_file = shellquote($crt_file) 73 | $_crt_chain_file = shellquote($crt_chain_file) 74 | 75 | $le_check_command = join([ 76 | "/usr/bin/test -f ${_crt_file}", 77 | '&&', 78 | "/usr/bin/openssl x509 -checkend 2592000 -noout -in ${_crt_file}", 79 | '&&', 80 | "${letsencrypt_check_altnames} ${_csr_file} ${_crt_file}", 81 | '&&', 82 | '/usr/bin/test', 83 | '$(', 84 | "/usr/bin/stat -c '%Y' ${_crt_file}", 85 | ')', 86 | '-gt', 87 | '$(', 88 | "/usr/bin/stat -c '%Y' ${_csr_file}", 89 | ')', 90 | ], ' ') 91 | 92 | if ($altnames and !empty($altnames)) { 93 | $validate_domains = shellquote(flatten([$domain, $altnames])) 94 | } else { 95 | $validate_domains = shellquote([$domain,]) 96 | } 97 | 98 | 99 | $le_command = join([ 100 | $domain_validation_hook, 101 | $validate_domains, 102 | '&&', 103 | $dehydrated, 104 | '-d', shellquote($domain), 105 | '-k', shellquote($dehydrated_hook), 106 | '-t', shellquote($challengetype), 107 | '-f', shellquote($dehydrated_conf), 108 | '-a rsa', 109 | '--signcsr', 110 | $_csr_file, 111 | "> ${_crt_file}.new", 112 | "&& /bin/mv ${_crt_file}.new ${_crt_file}", 113 | ], ' ') 114 | 115 | exec { "create-certificate-${domain}" : 116 | user => $::letsencrypt::user, 117 | cwd => $dehydrated_dir, 118 | group => $::letsencrypt::group, 119 | unless => $le_check_command, 120 | command => $le_command, 121 | environment => $dehydrated_hook_env, 122 | require => [ 123 | User[$::letsencrypt::user], 124 | Group[$::letsencrypt::group], 125 | File[$csr_file], 126 | Vcsrepo[$dehydrated_dir], 127 | File[$dehydrated_hook], 128 | File[$dehydrated_conf], 129 | ], 130 | timeout => length(split($validate_domains, ' ')) * 90, 131 | } 132 | 133 | $get_certificate_chain_command = join([ 134 | $letsencrypt_chain_request, 135 | $_crt_file, 136 | $_crt_chain_file, 137 | ], ' ') 138 | exec { "get-certificate-chain-${domain}" : 139 | require => File[$letsencrypt_chain_request], 140 | subscribe => [ 141 | Exec["create-certificate-${domain}"], 142 | File[$letsencrypt_chain_request] 143 | ], 144 | refreshonly => true, 145 | user => $::letsencrypt::user, 146 | group => $::letsencrypt::group, 147 | command => $get_certificate_chain_command, 148 | timeout => 5*60, 149 | tries => 2, 150 | } 151 | 152 | 153 | # remove dh files from old module versions 154 | # TODO: remove again in the future. 155 | $dh_file = "${base_dir}/${domain}.dh" 156 | file { $dh_file: 157 | ensure => absent, 158 | force => true, 159 | } 160 | 161 | file { $crt_file : 162 | mode => '0644', 163 | replace => false, 164 | require => Exec["create-certificate-${domain}"], 165 | } 166 | 167 | ::letsencrypt::request::ocsp { $domain : 168 | require => File[$crt_file], 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /manifests/request/crt.pp: -------------------------------------------------------------------------------- 1 | # Define: letsencrypt::request::crt 2 | # 3 | # Take certificates form facter and export a ressource 4 | # with the certificate content. 5 | # 6 | 7 | define letsencrypt::request::crt( 8 | $domain = $name 9 | ) { 10 | 11 | require ::letsencrypt::params 12 | 13 | $handler_requests_dir = $::letsencrypt::params::handler_requests_dir 14 | $base_dir = "${handler_requests_dir}/${domain}" 15 | $crt_file = "${base_dir}/${domain}.crt" 16 | $ocsp_file = "${base_dir}/${domain}.crt.ocsp" 17 | $crt_chain_file = "${base_dir}/${domain}_ca.pem" 18 | 19 | $crt = file($crt_file) 20 | 21 | # special handling for ocsp stuff - binary junk... 22 | $ocsp = base64('encode', file_or_empty_string($ocsp_file)) 23 | 24 | $crt_chain = file_or_empty_string($crt_chain_file) 25 | 26 | if ($crt =~ /BEGIN CERTIFICATE/) { 27 | @@letsencrypt::deploy::crt { $domain : 28 | crt_content => $crt, 29 | crt_chain_content => $crt_chain, 30 | ocsp_content => $ocsp, 31 | tag => $::fqdn, 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /manifests/request/handler.pp: -------------------------------------------------------------------------------- 1 | # == Class: letsencrypt 2 | # 3 | # Include this class if you would like to create 4 | # Certificates or on your puppetmaster to have you CSRs signed. 5 | # 6 | # 7 | # === Parameters 8 | # 9 | # [*dehydrated_git_url*] 10 | # URL used to checkout the dehydrated using git. 11 | # Defaults to the upstream github url. 12 | # 13 | # [*hook_source*] 14 | # Points to the source of the dehydrated hook you'd like to 15 | # distribute ((as in file { ...: source => }) 16 | # hook_source or hook_content needs to be specified. 17 | # 18 | # [*hook_content*] 19 | # The actual content (as in file { ...: content => }) of the 20 | # dehydrated hook. 21 | # hook_source or hook_content needs to be specified. 22 | # 23 | # === Authors 24 | # 25 | # Author Name Bernd Zeimetz 26 | # 27 | # === Copyright 28 | # 29 | # Copyright 2016 Bernd Zeimetz 30 | # 31 | 32 | 33 | class letsencrypt::request::handler( 34 | $dehydrated_git_url, 35 | $letsencrypt_cas, 36 | $letsencrypt_ca, 37 | $hook_source, 38 | $hook_content, 39 | $letsencrypt_contact_email, 40 | $letsencrypt_proxy, 41 | $domain_validation_hook_source = undef, 42 | $domain_validation_hook_content = undef, 43 | $domain_validation_hook = $::letsencrypt::params::domain_validation_hook, 44 | $handler_base_dir = $::letsencrypt::params::handler_base_dir, 45 | $handler_requests_dir = $::letsencrypt::params::handler_requests_dir, 46 | $dehydrated_dir = $::letsencrypt::params::dehydrated_dir, 47 | $dehydrated_hook = $::letsencrypt::params::dehydrated_hook, 48 | $dehydrated_conf = $::letsencrypt::params::dehydrated_conf, 49 | $letsencrypt_chain_request = $::letsencrypt::params::letsencrypt_chain_request, 50 | $letsencrypt_ocsp_request = $::letsencrypt::params::letsencrypt_ocsp_request, 51 | $letsencrypt_check_altnames = $::letsencrypt::params::letsencrypt_check_altnames, 52 | ) inherits ::letsencrypt::params { 53 | 54 | require ::letsencrypt::params 55 | 56 | if (!empty($letsencrypt_proxy)) { 57 | $letsencrypt_proxy_without_protocol = regsubst($letsencrypt_proxy, '^.*://', '') 58 | } 59 | 60 | if $::letsencrypt::manage_user { 61 | user { $::letsencrypt::user: 62 | gid => $::letsencrypt::group, 63 | home => $handler_base_dir, 64 | shell => '/bin/bash', 65 | managehome => false, 66 | password => '!!', 67 | } 68 | } 69 | 70 | File { 71 | owner => root, 72 | group => root, 73 | } 74 | 75 | file { $handler_base_dir : 76 | ensure => directory, 77 | mode => '0755', 78 | owner => $::letsencrypt::user, 79 | group => $::letsencrypt::group, 80 | } 81 | file { "${handler_base_dir}/.acme-challenges" : 82 | ensure => directory, 83 | mode => '0755', 84 | owner => $::letsencrypt::user, 85 | group => $::letsencrypt::group, 86 | } 87 | file { $handler_requests_dir : 88 | ensure => directory, 89 | mode => '0755', 90 | } 91 | 92 | file { $dehydrated_hook : 93 | ensure => file, 94 | group => $::letsencrypt::group, 95 | require => Group[$::letsencrypt::group], 96 | source => $hook_source, 97 | content => $hook_content, 98 | mode => '0750', 99 | } 100 | 101 | if (!empty($domain_validation_hook_source) or !empty($domain_validation_hook_content)) { 102 | file { $domain_validation_hook : 103 | ensure => file, 104 | group => $::letsencrypt::group, 105 | require => Group[$::letsencrypt::group], 106 | source => $domain_validation_hook_source, 107 | content => $domain_validation_hook_content, 108 | mode => '0750', 109 | before => File[$dehydrated_hook], 110 | } 111 | } else { 112 | $exit0 = join(['#!/bin/bash', '', 'exit 0', '',], "\n") 113 | 114 | file { $domain_validation_hook : 115 | ensure => file, 116 | group => $::letsencrypt::group, 117 | require => Group[$::letsencrypt::group], 118 | content => $exit0, 119 | mode => '0750', 120 | before => File[$dehydrated_hook], 121 | } 122 | } 123 | 124 | 125 | vcsrepo { $dehydrated_dir : 126 | ensure => latest, 127 | revision => master, 128 | provider => git, 129 | source => $dehydrated_git_url, 130 | user => root, 131 | require => [ 132 | File[$handler_base_dir], 133 | Package['git'] 134 | ], 135 | } 136 | 137 | # handle switching CAs with different account keys. 138 | $ca_hash = $letsencrypt_cas[$letsencrypt_ca]['hash'] 139 | $ca_url = $letsencrypt_cas[$letsencrypt_ca]['url'] 140 | 141 | file { $dehydrated_conf : 142 | ensure => file, 143 | owner => root, 144 | group => $::letsencrypt::group, 145 | mode => '0640', 146 | content => template('letsencrypt/letsencrypt.conf.erb'), 147 | } 148 | 149 | file { $letsencrypt_chain_request : 150 | ensure => file, 151 | owner => root, 152 | group => $::letsencrypt::group, 153 | mode => '0755', 154 | content => template('letsencrypt/letsencrypt_get_certificate_chain.sh.erb'), 155 | } 156 | 157 | $openssl_11 = (versioncmp($::openssl_version, '1.1') >=0) 158 | 159 | file { $letsencrypt_ocsp_request : 160 | ensure => file, 161 | owner => root, 162 | group => $::letsencrypt::group, 163 | mode => '0755', 164 | content => template('letsencrypt/letsencrypt_get_certificate_ocsp.sh.erb'), 165 | } 166 | 167 | Letsencrypt::Request<<| tag == "crt-host-${::fqdn}" |>> 168 | 169 | file { $letsencrypt_check_altnames : 170 | ensure => file, 171 | owner => root, 172 | group => letsencrypt, 173 | mode => '0755', 174 | content => file('letsencrypt/letsencrypt_check_altnames.sh'), 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /manifests/request/ocsp.pp: -------------------------------------------------------------------------------- 1 | # Define: letsencrypt::request::ocsp 2 | # 3 | # Retrieve ocsp stapling information 4 | # 5 | 6 | define letsencrypt::request::ocsp( 7 | $domain = $name 8 | ) { 9 | 10 | require ::letsencrypt::params 11 | 12 | $handler_requests_dir = $::letsencrypt::params::handler_requests_dir 13 | $base_dir = "${handler_requests_dir}/${domain}" 14 | $crt_file = "${base_dir}/${domain}.crt" 15 | $crt_chain_file = "${base_dir}/${domain}_ca.pem" 16 | $ocsp_file = "${crt_file}.ocsp" 17 | $letsencrypt_ocsp_request = $::letsencrypt::params::letsencrypt_ocsp_request 18 | 19 | $ocsp_command = join([ 20 | $letsencrypt_ocsp_request, 21 | $crt_file, 22 | $crt_chain_file, 23 | $ocsp_file, 24 | ], ' ') 25 | $ocsp_onlyif = join([ 26 | '/usr/bin/test', 27 | '-f', 28 | "'${crt_file}'", 29 | ], ' ') 30 | 31 | $ocsp_unless = join([ 32 | '/usr/bin/test', 33 | '-f', 34 | "'${ocsp_file}'", 35 | '&&', 36 | '/usr/bin/test', 37 | '$(', 38 | "/usr/bin/stat -c '%Y' ${ocsp_file}", 39 | ')', 40 | '-gt', 41 | '$(', 42 | "/bin/date --date='1 day ago' '+%s'", 43 | ')', 44 | ], ' ') 45 | 46 | exec { "update_ocsp_file_for_${domain}" : 47 | command => $ocsp_command, 48 | unless => $ocsp_unless, 49 | onlyif => $ocsp_onlyif, 50 | user => $::letsencrypt::user, 51 | group => $::letsencrypt::group, 52 | require => File[$letsencrypt_ocsp_request], 53 | tries => 2, 54 | } 55 | 56 | file { $ocsp_file : 57 | mode => '0644', 58 | replace => false, 59 | require => Exec["update_ocsp_file_for_${domain}"], 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /manifests/setup.pp: -------------------------------------------------------------------------------- 1 | # == Class: letsencrypt::setup 2 | # 3 | # setup all necessary directories and groups 4 | # 5 | # === Authors 6 | # 7 | # Author Name Bernd Zeimetz 8 | # 9 | # === Copyright 10 | # 11 | # Copyright 2016 Bernd Zeimetz 12 | # 13 | class letsencrypt::setup ( 14 | $base_dir = $::letsencrypt::params::base_dir, 15 | $csr_dir = $::letsencrypt::params::csr_dir, 16 | $crt_dir = $::letsencrypt::params::crt_dir, 17 | $key_dir = $::letsencrypt::params::key_dir 18 | ) inherits ::letsencrypt::params { 19 | 20 | if $::letsencrypt::manage_user { 21 | group { $::letsencrypt::group : 22 | ensure => present, 23 | } 24 | } 25 | 26 | File { 27 | ensure => directory, 28 | owner => 'root', 29 | group => $::letsencrypt::group, 30 | mode => '0755', 31 | require => Group[$::letsencrypt::group], 32 | } 33 | 34 | file { $base_dir : } 35 | file { $csr_dir : } 36 | file { $crt_dir : } 37 | file { $key_dir : 38 | mode => '0750', 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /manifests/setup/puppetmaster.pp: -------------------------------------------------------------------------------- 1 | # == Class: letsencrypt::setup 2 | # 3 | # setup all necessary directories and groups 4 | # 5 | # === Authors 6 | # 7 | # Author Name Bernd Zeimetz 8 | # 9 | # === Copyright 10 | # 11 | # Copyright 2016 Bernd Zeimetz 12 | # 13 | class letsencrypt::setup::puppetmaster ( 14 | $manage_packages = true, 15 | ){ 16 | 17 | if ($manage_packages) { 18 | ensure_packages('git') 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bzed-letsencrypt", 3 | "version": "1.0.0", 4 | "author": "Bernd Zeimetz", 5 | "summary": "DEPRECATED - please use bzed-dehydrated instead!", 6 | "license": "Apache-2.0", 7 | "source": "https://github.com/bzed/bzed-letsencrypt", 8 | "project_page": "https://github.com/bzed/bzed-letsencrypt", 9 | "issues_url": "https://github.com/bzed/bzed-letsencrypt/issues", 10 | "dependencies": [ 11 | {"name":"puppetlabs-stdlib","version_requirement":">= 2.2.0"}, 12 | {"name":"puppetlabs-concat","version_requirement":">= 2.1.0"}, 13 | {"name":"puppetlabs-vcsrepo","version_requirement":">= 1.3.2"}, 14 | {"name":"camptocamp-openssl","version_requirement":">= 1.5.1"} 15 | ], 16 | "operatingsystem_support": [ 17 | { 18 | "operatingsystem": "RedHat", 19 | "operatingsystemrelease": [ 20 | "4", 21 | "5", 22 | "6", 23 | "7" 24 | ] 25 | }, 26 | { 27 | "operatingsystem": "CentOS", 28 | "operatingsystemrelease": [ 29 | "4", 30 | "5", 31 | "6", 32 | "7" 33 | ] 34 | }, 35 | { 36 | "operatingsystem": "OracleLinux", 37 | "operatingsystemrelease": [ 38 | "4", 39 | "5", 40 | "6", 41 | "7" 42 | ] 43 | }, 44 | { 45 | "operatingsystem": "Scientific", 46 | "operatingsystemrelease": [ 47 | "4", 48 | "5", 49 | "6", 50 | "7" 51 | ] 52 | }, 53 | { 54 | "operatingsystem": "SLES", 55 | "operatingsystemrelease": [ 56 | "10 SP4", 57 | "11 SP1", 58 | "12" 59 | ] 60 | }, 61 | { 62 | "operatingsystem": "Debian", 63 | "operatingsystemrelease": [ 64 | "6", 65 | "7", 66 | "8" 67 | ] 68 | }, 69 | { 70 | "operatingsystem": "Ubuntu", 71 | "operatingsystemrelease": [ 72 | "10.04", 73 | "12.04", 74 | "14.04" 75 | ] 76 | } 77 | ] 78 | } 79 | 80 | -------------------------------------------------------------------------------- /spec/classes/letsencrypt_init_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'letsencrypt', type: :class do 4 | let :facts do 5 | { 6 | osfamily: 'Debian', 7 | fqdn: 'spectest', 8 | puppetmaster: 'spectest', 9 | letsencrypt_crts: 'spectest', 10 | letsencrypt_csr_spectest: '-----BEGIN CERTIFICATE REQUEST-----\nMIICpzCCAY8CAQAwYjELMAkGA1UEBhMCQVQxETAPBgNVBAgMCFNhbHpidXJnMREw\nDwYDVQQHDAhTYWx6YnVyZzENMAsGA1UECgwEYnplZDEeMBwGA1UEAwwVc3BlYy10\nZXN0LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\nlc3jPhFUfrfPvvQOd7vtl1+eA4ak6MAEqGL0c6xjyIiaHffNMr1ujPhZIyDuX+a/\nvbB4en+FZrq3abSqGEF0+ca/aqluAxR3jQzM231g18UpAppceV/Xz8lOsk4u5vl2\nnBW/44GwpwV+rFTjgsNPZ3dDRWaTcyJ8FFxLstf5AWudecnLYEiWeXIpiCYiUZZH\neoO9SUhs77f2S3lU8sAeUGn8L4Xrx70cyhDym7gh2Vwf0LmvulsNvQQPPPEfh3Mp\nHQvhqPmRYVl/fAszEEhrElgwCT76LPbvJjs/bYdkDUbT1gv1Lv/4h7xbvc7YsIf9\naIGQGU+Tbc4Bpd1uMQjxwwIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAF8ytNzr\n2HLQaqjPH6ETOi3yiheJe8tNB1bV8YCtffxjPyIkRUONtMTuugD1bWSBz9ZsBIGs\nsTsUH1nBXUneinVjCBDM8pWGyyQJ+Sp9mGzO/ozgl/B3Ty54/J7OfE6kAlGibSoX\nQOmUGLdsRDGgVmismofs2fam4XDjijVxXMgrdzj22KbcuWmdF3W+9Sn8GpEXW4Nh\nT+oIq/TPAl/wym6jo6EeflMlOLo0V4Bb5ylmkPCkLVGwm/ClouxgNSRP/uDDa/kT\nMhRrz3+JfoRSK2PgzT1Ai48eTt98YxsBr261KvovbMbnpqPtCfedta33d7EESg32\nssyZggp6vUJnypw=\n-----END CERTIFICATE REQUEST-----' 11 | } 12 | end 13 | # it { should contain_file('/etc/letsencrypt').with_ensure('directory') } 14 | # it { should contain_file('/etc/letsencrypt/csr').with_ensure('directory') } 15 | # it { should contain_file('/etc/letsencrypt/private').with_ensure('directory') } 16 | # it { should contain_file('/etc/letsencrypt/certs').with_ensure('directory') } 17 | end 18 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'puppetlabs_spec_helper/module_spec_helper' 2 | -------------------------------------------------------------------------------- /templates/cert.cnf.erb: -------------------------------------------------------------------------------- 1 | # file managed by puppet 2 | 3 | # This definition stops the following lines choking if HOME isn't 4 | # defined. 5 | HOME = . 6 | RANDFILE = $ENV::HOME/.rnd 7 | 8 | [ req ] 9 | default_bits = 4096 10 | default_md = sha256 11 | default_keyfile = privkey.pem 12 | distinguished_name = req_distinguished_name 13 | prompt = no 14 | <% if @req_ext -%> 15 | req_extensions = v3_req 16 | <% end -%> 17 | 18 | [ req_distinguished_name ] 19 | commonName = <%= @domain %> 20 | <% unless @country.nil? -%> 21 | countryName = <%= @country %> 22 | <% end -%> 23 | <% unless @state.nil? -%> 24 | stateOrProvinceName = <%= @state %> 25 | <% end -%> 26 | <% unless @locality.nil? -%> 27 | localityName = <%= @locality %> 28 | <% end -%> 29 | <% unless @organization.nil? -%> 30 | organizationName = <%= @organization %> 31 | <% end -%> 32 | <% unless @unit.nil? -%> 33 | organizationalUnitName = <%= @unit %> 34 | <% end -%> 35 | <% unless @email.nil? -%> 36 | emailAddress = <%= @email %> 37 | <% end -%> 38 | 39 | <% if @req_ext -%> 40 | [ v3_req ] 41 | subjectAltName = @alt_names 42 | 43 | [ alt_names ] 44 | <%- i=1 -%> 45 | <% @subject_alt_names.each do |val| -%> 46 | DNS.<%= i -%> = <%= val %> 47 | <%- i = i+1 -%> 48 | <% end -%> 49 | 50 | <% end %> 51 | 52 | -------------------------------------------------------------------------------- /templates/letsencrypt.conf.erb: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### MANAGED BY PUPPET - DO NOT HAND HACK! 4 | 5 | 6 | # Path to certificate authority (default: https://acme-v01.api.letsencrypt.org/directory) 7 | CA="<%= @ca_url -%>" 8 | 9 | # Path to license agreement (default: https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf) 10 | LICENSE="https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf" 11 | 12 | # Which challenge should be used? Currently http-01 and dns-01 are supported 13 | #CHALLENGETYPE="http-01" 14 | 15 | # Path to a directory containing additional config files, allowing to override 16 | # the defaults found in the main configuration file. Additional config files 17 | # in this directory needs to be named with a '.sh' ending. 18 | # default: 19 | #CONFIG_D= 20 | 21 | # Base directory for account key, generated certificates and list of domains (default: $SCRIPTDIR -- uses config directory if undefined) 22 | BASEDIR="<%= @handler_base_dir -%>" 23 | 24 | # Output directory for challenge-tokens to be served by webserver or deployed in HOOK (default: $BASEDIR/.acme-challenges) 25 | #WELLKNOWN="${BASEDIR}/.acme-challenges" 26 | 27 | # Location of private account key (default: $BASEDIR/private_key.pem) 28 | PRIVATE_KEY="<%= @handler_base_dir -%>/accounts/<%= @ca_hash -%>/account_key.pem" 29 | 30 | # Location of private account registration information (default: $BASEDIR/private_key.json) 31 | PRIVATE_KEY_JSON="<%= @handler_base_dir -%>/accounts/<%= @ca_hash -%>/registration_info.json" 32 | 33 | # Default keysize for private keys (default: 4096) 34 | #KEYSIZE="4096" 35 | 36 | # Path to openssl config file (default: - tries to figure out system default) 37 | #OPENSSL_CNF= 38 | 39 | # Program or function called in certain situations 40 | # 41 | # After generating the challenge-response, or after failed challenge (in this case altname is empty) 42 | # Given arguments: clean_challenge|deploy_challenge altname token-filename token-content 43 | # 44 | # After successfully signing certificate 45 | # Given arguments: deploy_cert domain path/to/privkey.pem path/to/cert.pem path/to/fullchain.pem 46 | # 47 | # BASEDIR and WELLKNOWN variables are exported and can be used in an external program 48 | # default: 49 | #HOOK= 50 | 51 | # Minimum days before expiration to automatically renew certificate (default: 30) 52 | #RENEW_DAYS="30" 53 | 54 | # Regenerate private keys instead of just signing new certificates on renewal (default: no) 55 | #PRIVATE_KEY_RENEW="no" 56 | 57 | # Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1 58 | #KEY_ALGO=rsa 59 | 60 | # E-mail to use during the registration (default: ) 61 | <% if @letsencrypt_contact_email -%> 62 | CONTACT_EMAIL=<%= @letsencrypt_contact_email %> 63 | <%- else -%> 64 | #CONTACT_EMAIL= 65 | <%- end %> 66 | 67 | # Lockfile location, to prevent concurrent access (default: $BASEDIR/lock) 68 | #LOCKFILE="${BASEDIR}/lock" 69 | 70 | # use proxy settings if necessary. 71 | <%if @letsencrypt_proxy -%> 72 | http_proxy="<%= @letsencrypt_proxy -%>" 73 | https_proxy="<%= @letsencrypt_proxy -%>" 74 | export http_proxy 75 | export https_proxy 76 | <%- end %> 77 | -------------------------------------------------------------------------------- /templates/letsencrypt_get_certificate_chain.sh.erb: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 5 | 6 | <% if @letsencrypt_proxy -%> 7 | export http_proxy="<%= @letsencrypt_proxy -%>" 8 | export https_proxy="<%= @letsencrypt_proxy -%>" 9 | <% end -%> 10 | 11 | if [ $# -lt 2 ]; then exit 255; fi 12 | 13 | if [ $# -gt 2 ]; then 14 | export http_proxy="${3}" 15 | export https_proxy="${3}" 16 | fi 17 | 18 | # exit if both files exist and the chain file is more recent. 19 | if [ -r "${1}" -a -r "${2}" ]; then 20 | if [ $(stat -c '%Y' "${1}") -le $(stat -c '%Y' "${2}") ]; then 21 | exit 0 22 | fi 23 | fi 24 | 25 | CHAINURL=$(openssl x509 -in "${1}" -noout -text | grep 'CA Issuers - URI:' | cut -d':' -f2-) 26 | wget -q -O "${2}.new" "${CHAINURL}" 27 | 28 | if ! grep -q "BEGIN CERTIFICATE" "${2}.new"; then 29 | openssl x509 -in "${2}.new" -inform DER -out "${2}.new.pem" -outform PEM 30 | fi 31 | mv "${2}.new.pem" "${2}" 32 | rm "${2}.new" 33 | 34 | -------------------------------------------------------------------------------- /templates/letsencrypt_get_certificate_ocsp.sh.erb: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 5 | 6 | <% if @letsencrypt_proxy -%> 7 | export http_proxy="<%= @letsencrypt_proxy -%>" 8 | export https_proxy="<%= @letsencrypt_proxy -%>" 9 | <% end -%> 10 | 11 | if [ $# -lt 3 ]; then exit 255; fi 12 | 13 | OCSPURL=$(openssl x509 -in "${1}" -noout -text | grep "OCSP - URI:" | cut -d: -f2,3 ) 14 | OCSPHOST=$(echo $OCSPURL | sed 's,^.*://\([^/]*\).*,\1,') 15 | 16 | 17 | <% if @letsencrypt_proxy -%> 18 | openssl ocsp -noverify -issuer "${2}" \ 19 | -cert "${1}" \ 20 | -host "<%= @letsencrypt_proxy_without_protocol -%>" \ 21 | -path "${OCSPURL}" -respout "${3}.new" 22 | <% else -%> 23 | openssl ocsp -noverify -issuer "${2}" \ 24 | -cert "${1}" \ 25 | <% if @openssl_11 -%> 26 | -header Host="${OCSPHOST}" \ 27 | <% else -%> 28 | -header Host "${OCSPHOST}" \ 29 | <% end -%> 30 | -url "${OCSPURL}" -respout "${3}.new" 31 | 32 | <% end -%> 33 | 34 | mv "${3}.new" "${3}" 35 | 36 | --------------------------------------------------------------------------------