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