at 18.09-beta 16 kB view raw
1# Fully pluggable module to have Letsencrypt's Boulder ACME service running in 2# a test environment. 3# 4# The certificate for the ACME service is exported as: 5# 6# config.test-support.letsencrypt.caCert 7# 8# This value can be used inside the configuration of other test nodes to inject 9# the snakeoil certificate into security.pki.certificateFiles or into package 10# overlays. 11# 12# Another value that's needed if you don't use a custom resolver (see below for 13# notes on that) is to add the letsencrypt node as a nameserver to every node 14# that needs to acquire certificates using ACME, because otherwise the API host 15# for letsencrypt.org can't be resolved. 16# 17# A configuration example of a full node setup using this would be this: 18# 19# { 20# letsencrypt = import ./common/letsencrypt; 21# 22# example = { nodes, ... }: { 23# networking.nameservers = [ 24# nodes.letsencrypt.config.networking.primaryIPAddress 25# ]; 26# security.pki.certificateFiles = [ 27# nodes.letsencrypt.config.test-support.letsencrypt.caCert 28# ]; 29# }; 30# } 31# 32# By default, this module runs a local resolver, generated using resolver.nix 33# from the parent directory to automatically discover all zones in the network. 34# 35# If you do not want this and want to use your own resolver, you can just 36# override networking.nameservers like this: 37# 38# { 39# letsencrypt = { nodes, ... }: { 40# imports = [ ./common/letsencrypt ]; 41# networking.nameservers = [ 42# nodes.myresolver.config.networking.primaryIPAddress 43# ]; 44# }; 45# 46# myresolver = ...; 47# } 48# 49# Keep in mind, that currently only _one_ resolver is supported, if you have 50# more than one resolver in networking.nameservers only the first one will be 51# used. 52# 53# Also make sure that whenever you use a resolver from a different test node 54# that it has to be started _before_ the ACME service. 55{ config, pkgs, lib, ... }: 56 57let 58 softhsm = pkgs.stdenv.mkDerivation rec { 59 name = "softhsm-${version}"; 60 version = "1.3.8"; 61 62 src = pkgs.fetchurl { 63 url = "https://dist.opendnssec.org/source/${name}.tar.gz"; 64 sha256 = "0flmnpkgp65ym7w3qyg78d3fbmvq3aznmi66rgd420n33shf7aif"; 65 }; 66 67 configureFlags = [ "--with-botan=${pkgs.botan}" ]; 68 buildInputs = [ pkgs.sqlite ]; 69 }; 70 71 pkcs11-proxy = pkgs.stdenv.mkDerivation { 72 name = "pkcs11-proxy"; 73 74 src = pkgs.fetchFromGitHub { 75 owner = "SUNET"; 76 repo = "pkcs11-proxy"; 77 rev = "944684f78bca0c8da6cabe3fa273fed3db44a890"; 78 sha256 = "1nxgd29y9wmifm11pjcdpd2y293p0dgi0x5ycis55miy97n0f5zy"; 79 }; 80 81 postPatch = "patchShebangs mksyscalls.sh"; 82 83 nativeBuildInputs = [ pkgs.cmake ]; 84 buildInputs = [ pkgs.openssl pkgs.libseccomp ]; 85 }; 86 87 mkGoDep = { goPackagePath, url ? "https://${goPackagePath}", rev, sha256 }: { 88 inherit goPackagePath; 89 src = pkgs.fetchgit { inherit url rev sha256; }; 90 }; 91 92 goose = let 93 owner = "liamstask"; 94 repo = "goose"; 95 rev = "8488cc47d90c8a502b1c41a462a6d9cc8ee0a895"; 96 version = "20150116"; 97 98 in pkgs.buildGoPackage rec { 99 name = "${repo}-${version}"; 100 101 src = pkgs.fetchFromBitbucket { 102 name = "${name}-src"; 103 inherit rev owner repo; 104 sha256 = "1jy0pscxjnxjdg3hj111w21g8079rq9ah2ix5ycxxhbbi3f0wdhs"; 105 }; 106 107 goPackagePath = "bitbucket.org/${owner}/${repo}"; 108 subPackages = [ "cmd/goose" ]; 109 extraSrcs = map mkGoDep [ 110 { goPackagePath = "github.com/go-sql-driver/mysql"; 111 rev = "2e00b5cd70399450106cec6431c2e2ce3cae5034"; 112 sha256 = "085g48jq9hzmlcxg122n0c4pi41sc1nn2qpx1vrl2jfa8crsppa5"; 113 } 114 { goPackagePath = "github.com/kylelemons/go-gypsy"; 115 rev = "08cad365cd28a7fba23bb1e57aa43c5e18ad8bb8"; 116 sha256 = "1djv7nii3hy451n5jlslk0dblqzb1hia1cbqpdwhnps1g8hqjy8q"; 117 } 118 { goPackagePath = "github.com/lib/pq"; 119 rev = "ba5d4f7a35561e22fbdf7a39aa0070f4d460cfc0"; 120 sha256 = "1mfbqw9g00bk24bfmf53wri5c2wqmgl0qh4sh1qv2da13a7cwwg3"; 121 } 122 { goPackagePath = "github.com/mattn/go-sqlite3"; 123 rev = "2acfafad5870400156f6fceb12852c281cbba4d5"; 124 sha256 = "1rpgil3w4hh1cibidskv1js898hwz83ps06gh0hm3mym7ki8d5h7"; 125 } 126 { goPackagePath = "github.com/ziutek/mymysql"; 127 rev = "0582bcf675f52c0c2045c027fd135bd726048f45"; 128 sha256 = "0bkc9x8sgqbzgdimsmsnhb0qrzlzfv33fgajmmjxl4hcb21qz3rf"; 129 } 130 { goPackagePath = "golang.org/x/net"; 131 url = "https://go.googlesource.com/net"; 132 rev = "10c134ea0df15f7e34d789338c7a2d76cc7a3ab9"; 133 sha256 = "14cbr2shl08gyg85n5gj7nbjhrhhgrd52h073qd14j97qcxsakcz"; 134 } 135 ]; 136 }; 137 138 boulder = let 139 owner = "letsencrypt"; 140 repo = "boulder"; 141 rev = "9c6a1f2adc4c26d925588f5ae366cfd4efb7813a"; 142 version = "20180129"; 143 144 in pkgs.buildGoPackage rec { 145 name = "${repo}-${version}"; 146 147 src = pkgs.fetchFromGitHub { 148 name = "${name}-src"; 149 inherit rev owner repo; 150 sha256 = "09kszswrifm9rc6idfaq0p1mz5w21as2qbc8gd5pphrq9cf9pn55"; 151 }; 152 153 postPatch = '' 154 # compat for go < 1.8 155 sed -i -e 's/time\.Until(\([^)]\+\))/\1.Sub(time.Now())/' \ 156 test/ocsp/helper/helper.go 157 158 find test -type f -exec sed -i -e '/libpkcs11-proxy.so/ { 159 s,/usr/local,${pkcs11-proxy}, 160 }' {} + 161 162 sed -i -r \ 163 -e '/^def +install/a \ return True' \ 164 -e 's,exec \./bin/,,' \ 165 test/startservers.py 166 167 cat ${lib.escapeShellArg snakeOilCerts.ca.key} > test/test-ca.key 168 cat ${lib.escapeShellArg snakeOilCerts.ca.cert} > test/test-ca.pem 169 ''; 170 171 # Until vendored pkcs11 is go 1.9 compatible 172 preBuild = '' 173 rm -r go/src/github.com/letsencrypt/boulder/vendor/github.com/miekg/pkcs11 174 ''; 175 176 # XXX: Temporarily brought back putting the source code in the output, 177 # since e95f17e2720e67e2eabd59d7754c814d3e27a0b2 was removing that from 178 # buildGoPackage. 179 preInstall = '' 180 mkdir -p $out 181 pushd "$NIX_BUILD_TOP/go" 182 while read f; do 183 echo "$f" | grep -q '^./\(src\|pkg/[^/]*\)/${goPackagePath}' \ 184 || continue 185 mkdir -p "$(dirname "$out/share/go/$f")" 186 cp "$NIX_BUILD_TOP/go/$f" "$out/share/go/$f" 187 done < <(find . -type f) 188 popd 189 ''; 190 191 extraSrcs = map mkGoDep [ 192 { goPackagePath = "github.com/miekg/pkcs11"; 193 rev = "6dbd569b952ec150d1425722dbbe80f2c6193f83"; 194 sha256 = "1m8g6fx7df6hf6q6zsbyw1icjmm52dmsx28rgb0h930wagvngfwb"; 195 } 196 ]; 197 198 goPackagePath = "github.com/${owner}/${repo}"; 199 buildInputs = [ pkgs.libtool ]; 200 }; 201 202 boulderSource = "${boulder.out}/share/go/src/${boulder.goPackagePath}"; 203 204 softHsmConf = pkgs.writeText "softhsm.conf" '' 205 0:/var/lib/softhsm/slot0.db 206 1:/var/lib/softhsm/slot1.db 207 ''; 208 209 snakeOilCerts = import ./snakeoil-certs.nix; 210 211 wfeDomain = "acme-v01.api.letsencrypt.org"; 212 wfeCertFile = snakeOilCerts.${wfeDomain}.cert; 213 wfeKeyFile = snakeOilCerts.${wfeDomain}.key; 214 215 siteDomain = "letsencrypt.org"; 216 siteCertFile = snakeOilCerts.${siteDomain}.cert; 217 siteKeyFile = snakeOilCerts.${siteDomain}.key; 218 219 # Retrieved via: 220 # curl -s -I https://acme-v01.api.letsencrypt.org/terms \ 221 # | sed -ne 's/^[Ll]ocation: *//p' 222 tosUrl = "https://letsencrypt.org/documents/2017.11.15-LE-SA-v1.2.pdf"; 223 tosPath = builtins.head (builtins.match "https?://[^/]+(.*)" tosUrl); 224 225 tosFile = pkgs.fetchurl { 226 url = tosUrl; 227 sha256 = "0yvyckqzj0b1xi61sypcha82nanizzlm8yqy828h2jbza7cxi26c"; 228 }; 229 230 resolver = let 231 message = "You need to define a resolver for the letsencrypt test module."; 232 firstNS = lib.head config.networking.nameservers; 233 in if config.networking.nameservers == [] then throw message else firstNS; 234 235 cfgDir = pkgs.stdenv.mkDerivation { 236 name = "boulder-config"; 237 src = "${boulderSource}/test/config"; 238 nativeBuildInputs = [ pkgs.jq ]; 239 phases = [ "unpackPhase" "patchPhase" "installPhase" ]; 240 postPatch = '' 241 sed -i -e 's/5002/80/' -e 's/5002/443/' va.json 242 sed -i -e '/listenAddress/s/:4000/:80/' wfe.json 243 sed -i -r \ 244 -e ${lib.escapeShellArg "s,http://boulder:4000/terms/v1,${tosUrl},g"} \ 245 -e 's,http://(boulder|127\.0\.0\.1):4000,https://${wfeDomain},g' \ 246 -e '/dnsResolver/s/127\.0\.0\.1:8053/${resolver}:53/' \ 247 *.json 248 if grep 4000 *.json; then exit 1; fi 249 250 # Change all ports from 1909X to 909X, because the 1909X range of ports is 251 # allocated by startservers.py in order to intercept gRPC communication. 252 sed -i -e 's/\<1\(909[0-9]\)\>/\1/' *.json 253 254 # Patch out all additional issuer certs 255 jq '. + {ca: (.ca + {Issuers: 256 [.ca.Issuers[] | select(.CertFile == "test/test-ca.pem")] 257 })}' ca.json > tmp 258 mv tmp ca.json 259 ''; 260 installPhase = "cp -r . \"$out\""; 261 }; 262 263 components = { 264 gsb-test-srv.args = "-apikey my-voice-is-my-passport"; 265 gsb-test-srv.waitForPort = 6000; 266 gsb-test-srv.first = true; 267 boulder-sa.args = "--config ${cfgDir}/sa.json"; 268 boulder-wfe.args = "--config ${cfgDir}/wfe.json"; 269 boulder-ra.args = "--config ${cfgDir}/ra.json"; 270 boulder-ca.args = "--config ${cfgDir}/ca.json"; 271 boulder-va.args = "--config ${cfgDir}/va.json"; 272 boulder-publisher.args = "--config ${cfgDir}/publisher.json"; 273 boulder-publisher.waitForPort = 9091; 274 ocsp-updater.args = "--config ${cfgDir}/ocsp-updater.json"; 275 ocsp-updater.after = [ "boulder-publisher" ]; 276 ocsp-responder.args = "--config ${cfgDir}/ocsp-responder.json"; 277 ct-test-srv = {}; 278 mail-test-srv.args = let 279 key = "${boulderSource}/test/mail-test-srv/minica-key.pem"; 280 crt = "${boulderSource}/test/mail-test-srv/minica.pem"; 281 in 282 "--closeFirst 5 --cert ${crt} --key ${key}"; 283 }; 284 285 commonPath = [ softhsm pkgs.mariadb goose boulder ]; 286 287 mkServices = a: b: with lib; listToAttrs (concatLists (mapAttrsToList a b)); 288 289 componentServices = mkServices (name: attrs: let 290 mkSrvName = n: "boulder-${n}.service"; 291 firsts = lib.filterAttrs (lib.const (c: c.first or false)) components; 292 firstServices = map mkSrvName (lib.attrNames firsts); 293 firstServicesNoSelf = lib.remove "boulder-${name}.service" firstServices; 294 additionalAfter = firstServicesNoSelf ++ map mkSrvName (attrs.after or []); 295 needsPort = attrs ? waitForPort; 296 inits = map (n: "boulder-init-${n}.service") [ "mysql" "softhsm" ]; 297 portWaiter = { 298 name = "boulder-${name}"; 299 value = { 300 description = "Wait For Port ${toString attrs.waitForPort} (${name})"; 301 after = [ "boulder-real-${name}.service" "bind.service" ]; 302 requires = [ "boulder-real-${name}.service" ]; 303 requiredBy = [ "boulder.service" ]; 304 serviceConfig.Type = "oneshot"; 305 serviceConfig.RemainAfterExit = true; 306 script = let 307 netcat = "${pkgs.libressl.nc}/bin/nc"; 308 portCheck = "${netcat} -z 127.0.0.1 ${toString attrs.waitForPort}"; 309 in "while ! ${portCheck}; do :; done"; 310 }; 311 }; 312 in lib.optional needsPort portWaiter ++ lib.singleton { 313 name = if needsPort then "boulder-real-${name}" else "boulder-${name}"; 314 value = { 315 description = "Boulder ACME Component (${name})"; 316 after = inits ++ additionalAfter; 317 requires = inits; 318 requiredBy = [ "boulder.service" ]; 319 path = commonPath; 320 environment.GORACE = "halt_on_error=1"; 321 environment.SOFTHSM_CONF = softHsmConf; 322 environment.PKCS11_PROXY_SOCKET = "tcp://127.0.0.1:5657"; 323 serviceConfig.WorkingDirectory = boulderSource; 324 serviceConfig.ExecStart = "${boulder}/bin/${name} ${attrs.args or ""}"; 325 serviceConfig.Restart = "on-failure"; 326 }; 327 }) components; 328 329in { 330 imports = [ ../resolver.nix ]; 331 332 options.test-support.letsencrypt.caCert = lib.mkOption { 333 type = lib.types.path; 334 description = '' 335 A certificate file to use with the <literal>nodes</literal> attribute to 336 inject the snakeoil CA certificate used in the ACME server into 337 <option>security.pki.certificateFiles</option>. 338 ''; 339 }; 340 341 config = { 342 test-support = { 343 resolver.enable = let 344 isLocalResolver = config.networking.nameservers == [ "127.0.0.1" ]; 345 in lib.mkOverride 900 isLocalResolver; 346 letsencrypt.caCert = snakeOilCerts.ca.cert; 347 }; 348 349 # This has priority 140, because modules/testing/test-instrumentation.nix 350 # already overrides this with priority 150. 351 networking.nameservers = lib.mkOverride 140 [ "127.0.0.1" ]; 352 networking.firewall.enable = false; 353 354 networking.extraHosts = '' 355 127.0.0.1 ${toString [ 356 "sa.boulder" "ra.boulder" "wfe.boulder" "ca.boulder" "va.boulder" 357 "publisher.boulder" "ocsp-updater.boulder" "admin-revoker.boulder" 358 "boulder" "boulder-mysql" wfeDomain 359 ]} 360 ${config.networking.primaryIPAddress} ${wfeDomain} ${siteDomain} 361 ''; 362 363 services.mysql.enable = true; 364 services.mysql.package = pkgs.mariadb; 365 366 services.nginx.enable = true; 367 services.nginx.recommendedProxySettings = true; 368 # This fixes the test on i686 369 services.nginx.commonHttpConfig = '' 370 server_names_hash_bucket_size 64; 371 ''; 372 services.nginx.virtualHosts.${wfeDomain} = { 373 onlySSL = true; 374 enableACME = false; 375 sslCertificate = wfeCertFile; 376 sslCertificateKey = wfeKeyFile; 377 locations."/".proxyPass = "http://127.0.0.1:80"; 378 }; 379 services.nginx.virtualHosts.${siteDomain} = { 380 onlySSL = true; 381 enableACME = false; 382 sslCertificate = siteCertFile; 383 sslCertificateKey = siteKeyFile; 384 locations.${tosPath}.extraConfig = "alias ${tosFile};"; 385 }; 386 387 systemd.services = { 388 pkcs11-daemon = { 389 description = "PKCS11 Daemon"; 390 after = [ "boulder-init-softhsm.service" ]; 391 before = map (n: "${n}.service") (lib.attrNames componentServices); 392 wantedBy = [ "multi-user.target" ]; 393 environment.SOFTHSM_CONF = softHsmConf; 394 environment.PKCS11_DAEMON_SOCKET = "tcp://127.0.0.1:5657"; 395 serviceConfig.ExecStart = let 396 softhsmLib = "${softhsm}/lib/softhsm/libsofthsm.so"; 397 in "${pkcs11-proxy}/bin/pkcs11-daemon ${softhsmLib}"; 398 }; 399 400 boulder-init-mysql = { 401 description = "Boulder ACME Init (MySQL)"; 402 after = [ "mysql.service" ]; 403 serviceConfig.Type = "oneshot"; 404 serviceConfig.RemainAfterExit = true; 405 serviceConfig.WorkingDirectory = boulderSource; 406 path = commonPath; 407 script = "${pkgs.bash}/bin/sh test/create_db.sh"; 408 }; 409 410 boulder-init-softhsm = { 411 description = "Boulder ACME Init (SoftHSM)"; 412 environment.SOFTHSM_CONF = softHsmConf; 413 serviceConfig.Type = "oneshot"; 414 serviceConfig.RemainAfterExit = true; 415 serviceConfig.WorkingDirectory = boulderSource; 416 preStart = "mkdir -p /var/lib/softhsm"; 417 path = commonPath; 418 script = '' 419 softhsm --slot 0 --init-token \ 420 --label intermediate --pin 5678 --so-pin 1234 421 softhsm --slot 0 --import test/test-ca.key \ 422 --label intermediate_key --pin 5678 --id FB 423 softhsm --slot 1 --init-token \ 424 --label root --pin 5678 --so-pin 1234 425 softhsm --slot 1 --import test/test-root.key \ 426 --label root_key --pin 5678 --id FA 427 ''; 428 }; 429 430 boulder = { 431 description = "Boulder ACME Server"; 432 after = map (n: "${n}.service") (lib.attrNames componentServices); 433 wantedBy = [ "multi-user.target" ]; 434 serviceConfig.Type = "oneshot"; 435 serviceConfig.RemainAfterExit = true; 436 script = let 437 ports = lib.range 8000 8005 ++ lib.singleton 80; 438 netcat = "${pkgs.libressl.nc}/bin/nc"; 439 mkPortCheck = port: "${netcat} -z 127.0.0.1 ${toString port}"; 440 checks = "(${lib.concatMapStringsSep " && " mkPortCheck ports})"; 441 in "while ! ${checks}; do :; done"; 442 }; 443 } // componentServices; 444 }; 445}