1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8with lib; 9 10let 11 top = config.services.kubernetes; 12 cfg = top.pki; 13 14 csrCA = pkgs.writeText "kube-pki-cacert-csr.json" ( 15 builtins.toJSON { 16 key = { 17 algo = "rsa"; 18 size = 2048; 19 }; 20 names = singleton cfg.caSpec; 21 } 22 ); 23 24 csrCfssl = pkgs.writeText "kube-pki-cfssl-csr.json" ( 25 builtins.toJSON { 26 key = { 27 algo = "rsa"; 28 size = 2048; 29 }; 30 CN = top.masterAddress; 31 hosts = [ top.masterAddress ] ++ cfg.cfsslAPIExtraSANs; 32 } 33 ); 34 35 cfsslAPITokenBaseName = "apitoken.secret"; 36 cfsslAPITokenPath = "${config.services.cfssl.dataDir}/${cfsslAPITokenBaseName}"; 37 certmgrAPITokenPath = "${top.secretsPath}/${cfsslAPITokenBaseName}"; 38 cfsslAPITokenLength = 32; 39 40 clusterAdminKubeconfig = 41 with cfg.certs.clusterAdmin; 42 top.lib.mkKubeConfig "cluster-admin" { 43 server = top.apiserverAddress; 44 certFile = cert; 45 keyFile = key; 46 }; 47 48 remote = with config.services; "https://${kubernetes.masterAddress}:${toString cfssl.port}"; 49in 50{ 51 ###### interface 52 options.services.kubernetes.pki = with lib.types; { 53 54 enable = mkEnableOption "easyCert issuer service"; 55 56 certs = mkOption { 57 description = "List of certificate specs to feed to cert generator."; 58 default = { }; 59 type = attrs; 60 }; 61 62 genCfsslCACert = mkOption { 63 description = '' 64 Whether to automatically generate cfssl CA certificate and key, 65 if they don't exist. 66 ''; 67 default = true; 68 type = bool; 69 }; 70 71 genCfsslAPICerts = mkOption { 72 description = '' 73 Whether to automatically generate cfssl API webserver TLS cert and key, 74 if they don't exist. 75 ''; 76 default = true; 77 type = bool; 78 }; 79 80 cfsslAPIExtraSANs = mkOption { 81 description = '' 82 Extra x509 Subject Alternative Names to be added to the cfssl API webserver TLS cert. 83 ''; 84 default = [ ]; 85 example = [ "subdomain.example.com" ]; 86 type = listOf str; 87 }; 88 89 genCfsslAPIToken = mkOption { 90 description = '' 91 Whether to automatically generate cfssl API-token secret, 92 if they doesn't exist. 93 ''; 94 default = true; 95 type = bool; 96 }; 97 98 pkiTrustOnBootstrap = mkOption { 99 description = "Whether to always trust remote cfssl server upon initial PKI bootstrap."; 100 default = true; 101 type = bool; 102 }; 103 104 caCertPathPrefix = mkOption { 105 description = '' 106 Path-prefrix for the CA-certificate to be used for cfssl signing. 107 Suffixes ".pem" and "-key.pem" will be automatically appended for 108 the public and private keys respectively. 109 ''; 110 default = "${config.services.cfssl.dataDir}/ca"; 111 defaultText = literalExpression ''"''${config.services.cfssl.dataDir}/ca"''; 112 type = str; 113 }; 114 115 caSpec = mkOption { 116 description = "Certificate specification for the auto-generated CAcert."; 117 default = { 118 CN = "kubernetes-cluster-ca"; 119 O = "NixOS"; 120 OU = "services.kubernetes.pki.caSpec"; 121 L = "auto-generated"; 122 }; 123 type = attrs; 124 }; 125 126 etcClusterAdminKubeconfig = mkOption { 127 description = '' 128 Symlink a kubeconfig with cluster-admin privileges to environment path 129 (/etc/\<path\>). 130 ''; 131 default = null; 132 type = nullOr str; 133 }; 134 135 }; 136 137 ###### implementation 138 config = mkIf cfg.enable ( 139 let 140 cfsslCertPathPrefix = "${config.services.cfssl.dataDir}/cfssl"; 141 cfsslCert = "${cfsslCertPathPrefix}.pem"; 142 cfsslKey = "${cfsslCertPathPrefix}-key.pem"; 143 in 144 { 145 146 services.cfssl = mkIf (top.apiserver.enable) { 147 enable = true; 148 address = "0.0.0.0"; 149 tlsCert = cfsslCert; 150 tlsKey = cfsslKey; 151 configFile = toString ( 152 pkgs.writeText "cfssl-config.json" ( 153 builtins.toJSON { 154 signing = { 155 profiles = { 156 default = { 157 usages = [ "digital signature" ]; 158 auth_key = "default"; 159 expiry = "720h"; 160 }; 161 }; 162 }; 163 auth_keys = { 164 default = { 165 type = "standard"; 166 key = "file:${cfsslAPITokenPath}"; 167 }; 168 }; 169 } 170 ) 171 ); 172 }; 173 174 systemd.services.cfssl.preStart = 175 with pkgs; 176 with config.services.cfssl; 177 mkIf (top.apiserver.enable) ( 178 concatStringsSep "\n" [ 179 "set -e" 180 (optionalString cfg.genCfsslCACert '' 181 if [ ! -f "${cfg.caCertPathPrefix}.pem" ]; then 182 ${cfssl}/bin/cfssl genkey -initca ${csrCA} | \ 183 ${cfssl}/bin/cfssljson -bare ${cfg.caCertPathPrefix} 184 fi 185 '') 186 (optionalString cfg.genCfsslAPICerts '' 187 if [ ! -f "${dataDir}/cfssl.pem" ]; then 188 ${cfssl}/bin/cfssl gencert -ca "${cfg.caCertPathPrefix}.pem" -ca-key "${cfg.caCertPathPrefix}-key.pem" ${csrCfssl} | \ 189 ${cfssl}/bin/cfssljson -bare ${cfsslCertPathPrefix} 190 fi 191 '') 192 (optionalString cfg.genCfsslAPIToken '' 193 if [ ! -f "${cfsslAPITokenPath}" ]; then 194 install -o cfssl -m 400 <(head -c ${ 195 toString (cfsslAPITokenLength / 2) 196 } /dev/urandom | od -An -t x | tr -d ' ') "${cfsslAPITokenPath}" 197 fi 198 '') 199 ] 200 ); 201 202 systemd.services.kube-certmgr-bootstrap = { 203 description = "Kubernetes certmgr bootstrapper"; 204 wantedBy = [ "certmgr.service" ]; 205 after = [ "cfssl.target" ]; 206 script = concatStringsSep "\n" [ 207 '' 208 set -e 209 210 # If there's a cfssl (cert issuer) running locally, then don't rely on user to 211 # manually paste it in place. Just symlink. 212 # otherwise, create the target file, ready for users to insert the token 213 214 mkdir -p "$(dirname "${certmgrAPITokenPath}")" 215 if [ -f "${cfsslAPITokenPath}" ]; then 216 ln -fs "${cfsslAPITokenPath}" "${certmgrAPITokenPath}" 217 elif [ ! -f "${certmgrAPITokenPath}" ]; then 218 # Don't remove the token if it already exists 219 install -m 600 /dev/null "${certmgrAPITokenPath}" 220 fi 221 '' 222 (optionalString (cfg.pkiTrustOnBootstrap) '' 223 if [ ! -f "${top.caFile}" ] || [ $(cat "${top.caFile}" | wc -c) -lt 1 ]; then 224 ${pkgs.curl}/bin/curl --fail-early -f -kd '{}' ${remote}/api/v1/cfssl/info | \ 225 ${pkgs.cfssl}/bin/cfssljson -stdout >${top.caFile} 226 fi 227 '') 228 ]; 229 serviceConfig = { 230 RestartSec = "10s"; 231 Restart = "on-failure"; 232 }; 233 }; 234 235 services.certmgr = { 236 enable = true; 237 package = pkgs.certmgr; 238 svcManager = "command"; 239 specs = 240 let 241 mkSpec = _: cert: { 242 inherit (cert) action; 243 authority = { 244 inherit remote; 245 root_ca = cert.caCert; 246 profile = "default"; 247 auth_key_file = certmgrAPITokenPath; 248 }; 249 certificate = { 250 path = cert.cert; 251 }; 252 private_key = cert.privateKeyOptions; 253 request = { 254 hosts = [ cert.CN ] ++ cert.hosts; 255 inherit (cert) CN; 256 key = { 257 algo = "rsa"; 258 size = 2048; 259 }; 260 names = [ cert.fields ]; 261 }; 262 }; 263 in 264 mapAttrs mkSpec cfg.certs; 265 }; 266 267 #TODO: Get rid of kube-addon-manager in the future for the following reasons 268 # - it is basically just a shell script wrapped around kubectl 269 # - it assumes that it is clusterAdmin or can gain clusterAdmin rights through serviceAccount 270 # - it is designed to be used with k8s system components only 271 # - it would be better with a more Nix-oriented way of managing addons 272 systemd.services.kube-addon-manager = mkIf top.addonManager.enable (mkMerge [ 273 { 274 environment.KUBECONFIG = 275 with cfg.certs.addonManager; 276 top.lib.mkKubeConfig "addon-manager" { 277 server = top.apiserverAddress; 278 certFile = cert; 279 keyFile = key; 280 }; 281 } 282 283 (optionalAttrs (top.addonManager.bootstrapAddons != { }) { 284 serviceConfig.PermissionsStartOnly = true; 285 preStart = 286 with pkgs; 287 let 288 files = mapAttrsToList ( 289 n: v: writeText "${n}.json" (builtins.toJSON v) 290 ) top.addonManager.bootstrapAddons; 291 in 292 '' 293 export KUBECONFIG=${clusterAdminKubeconfig} 294 ${top.package}/bin/kubectl apply -f ${concatStringsSep " \\\n -f " files} 295 ''; 296 }) 297 ]); 298 299 environment.etc.${cfg.etcClusterAdminKubeconfig}.source = mkIf ( 300 cfg.etcClusterAdminKubeconfig != null 301 ) clusterAdminKubeconfig; 302 303 environment.systemPackages = mkIf (top.kubelet.enable || top.proxy.enable) [ 304 (pkgs.writeScriptBin "nixos-kubernetes-node-join" '' 305 set -e 306 exec 1>&2 307 308 if [ $# -gt 0 ]; then 309 echo "Usage: $(basename $0)" 310 echo "" 311 echo "No args. Apitoken must be provided on stdin." 312 echo "To get the apitoken, execute: 'sudo cat ${certmgrAPITokenPath}' on the master node." 313 exit 1 314 fi 315 316 if [ $(id -u) != 0 ]; then 317 echo "Run as root please." 318 exit 1 319 fi 320 321 read -r token 322 if [ ''${#token} != ${toString cfsslAPITokenLength} ]; then 323 echo "Token must be of length ${toString cfsslAPITokenLength}." 324 exit 1 325 fi 326 327 install -m 0600 <(echo $token) ${certmgrAPITokenPath} 328 329 echo "Restarting certmgr..." >&1 330 systemctl restart certmgr 331 332 echo "Waiting for certs to appear..." >&1 333 334 ${optionalString top.kubelet.enable '' 335 while [ ! -f ${cfg.certs.kubelet.cert} ]; do sleep 1; done 336 echo "Restarting kubelet..." >&1 337 systemctl restart kubelet 338 ''} 339 340 ${optionalString top.proxy.enable '' 341 while [ ! -f ${cfg.certs.kubeProxyClient.cert} ]; do sleep 1; done 342 echo "Restarting kube-proxy..." >&1 343 systemctl restart kube-proxy 344 ''} 345 346 ${optionalString top.flannel.enable '' 347 while [ ! -f ${cfg.certs.flannelClient.cert} ]; do sleep 1; done 348 echo "Restarting flannel..." >&1 349 systemctl restart flannel 350 ''} 351 352 echo "Node joined successfully" 353 '') 354 ]; 355 356 # isolate etcd on loopback at the master node 357 # easyCerts doesn't support multimaster clusters anyway atm. 358 services.etcd = with cfg.certs.etcd; { 359 listenClientUrls = [ "https://127.0.0.1:2379" ]; 360 listenPeerUrls = [ "https://127.0.0.1:2380" ]; 361 advertiseClientUrls = [ "https://etcd.local:2379" ]; 362 initialCluster = [ "${top.masterAddress}=https://etcd.local:2380" ]; 363 initialAdvertisePeerUrls = [ "https://etcd.local:2380" ]; 364 certFile = mkDefault cert; 365 keyFile = mkDefault key; 366 trustedCaFile = mkDefault caCert; 367 }; 368 networking.extraHosts = mkIf (config.services.etcd.enable) '' 369 127.0.0.1 etcd.${top.addons.dns.clusterDomain} etcd.local 370 ''; 371 372 services.flannel = with cfg.certs.flannelClient; { 373 kubeconfig = top.lib.mkKubeConfig "flannel" { 374 server = top.apiserverAddress; 375 certFile = cert; 376 keyFile = key; 377 }; 378 }; 379 380 services.kubernetes = { 381 382 apiserver = mkIf top.apiserver.enable ( 383 with cfg.certs.apiServer; 384 { 385 etcd = with cfg.certs.apiserverEtcdClient; { 386 servers = [ "https://etcd.local:2379" ]; 387 certFile = mkDefault cert; 388 keyFile = mkDefault key; 389 caFile = mkDefault caCert; 390 }; 391 clientCaFile = mkDefault caCert; 392 tlsCertFile = mkDefault cert; 393 tlsKeyFile = mkDefault key; 394 serviceAccountKeyFile = mkDefault cfg.certs.serviceAccount.cert; 395 serviceAccountSigningKeyFile = mkDefault cfg.certs.serviceAccount.key; 396 kubeletClientCaFile = mkDefault caCert; 397 kubeletClientCertFile = mkDefault cfg.certs.apiserverKubeletClient.cert; 398 kubeletClientKeyFile = mkDefault cfg.certs.apiserverKubeletClient.key; 399 proxyClientCertFile = mkDefault cfg.certs.apiserverProxyClient.cert; 400 proxyClientKeyFile = mkDefault cfg.certs.apiserverProxyClient.key; 401 } 402 ); 403 controllerManager = mkIf top.controllerManager.enable { 404 serviceAccountKeyFile = mkDefault cfg.certs.serviceAccount.key; 405 rootCaFile = cfg.certs.controllerManagerClient.caCert; 406 kubeconfig = with cfg.certs.controllerManagerClient; { 407 certFile = mkDefault cert; 408 keyFile = mkDefault key; 409 }; 410 }; 411 scheduler = mkIf top.scheduler.enable { 412 kubeconfig = with cfg.certs.schedulerClient; { 413 certFile = mkDefault cert; 414 keyFile = mkDefault key; 415 }; 416 }; 417 kubelet = mkIf top.kubelet.enable { 418 clientCaFile = mkDefault cfg.certs.kubelet.caCert; 419 tlsCertFile = mkDefault cfg.certs.kubelet.cert; 420 tlsKeyFile = mkDefault cfg.certs.kubelet.key; 421 kubeconfig = with cfg.certs.kubeletClient; { 422 certFile = mkDefault cert; 423 keyFile = mkDefault key; 424 }; 425 }; 426 proxy = mkIf top.proxy.enable { 427 kubeconfig = with cfg.certs.kubeProxyClient; { 428 certFile = mkDefault cert; 429 keyFile = mkDefault key; 430 }; 431 }; 432 }; 433 } 434 ); 435 436 meta.buildDocsInSandbox = false; 437}