{ config, lib, pkgs, ... }: with lib; let top = config.services.kubernetes; cfg = top.pki; csrCA = pkgs.writeText "kube-pki-cacert-csr.json" (builtins.toJSON { key = { algo = "rsa"; size = 2048; }; names = singleton cfg.caSpec; }); csrCfssl = pkgs.writeText "kube-pki-cfssl-csr.json" (builtins.toJSON { key = { algo = "rsa"; size = 2048; }; CN = top.masterAddress; hosts = cfg.cfsslAPIExtraSANs; }); cfsslAPITokenBaseName = "apitoken.secret"; cfsslAPITokenPath = "${config.services.cfssl.dataDir}/${cfsslAPITokenBaseName}"; certmgrAPITokenPath = "${top.secretsPath}/${cfsslAPITokenBaseName}"; cfsslAPITokenLength = 32; clusterAdminKubeconfig = with cfg.certs.clusterAdmin; top.lib.mkKubeConfig "cluster-admin" { server = top.apiserverAddress; certFile = cert; keyFile = key; }; remote = with config.services; "https://${kubernetes.masterAddress}:${toString cfssl.port}"; in { ###### interface options.services.kubernetes.pki = with lib.types; { enable = mkEnableOption "easyCert issuer service"; certs = mkOption { description = "List of certificate specs to feed to cert generator."; default = {}; type = attrs; }; genCfsslCACert = mkOption { description = '' Whether to automatically generate cfssl CA certificate and key, if they don't exist. ''; default = true; type = bool; }; genCfsslAPICerts = mkOption { description = '' Whether to automatically generate cfssl API webserver TLS cert and key, if they don't exist. ''; default = true; type = bool; }; cfsslAPIExtraSANs = mkOption { description = '' Extra x509 Subject Alternative Names to be added to the cfssl API webserver TLS cert. ''; default = []; example = [ "subdomain.example.com" ]; type = listOf str; }; genCfsslAPIToken = mkOption { description = '' Whether to automatically generate cfssl API-token secret, if they doesn't exist. ''; default = true; type = bool; }; pkiTrustOnBootstrap = mkOption { description = "Whether to always trust remote cfssl server upon initial PKI bootstrap."; default = true; type = bool; }; caCertPathPrefix = mkOption { description = '' Path-prefrix for the CA-certificate to be used for cfssl signing. Suffixes ".pem" and "-key.pem" will be automatically appended for the public and private keys respectively. ''; default = "${config.services.cfssl.dataDir}/ca"; type = str; }; caSpec = mkOption { description = "Certificate specification for the auto-generated CAcert."; default = { CN = "kubernetes-cluster-ca"; O = "NixOS"; OU = "services.kubernetes.pki.caSpec"; L = "auto-generated"; }; type = attrs; }; etcClusterAdminKubeconfig = mkOption { description = '' Symlink a kubeconfig with cluster-admin privileges to environment path (/etc/<path>). ''; default = null; type = nullOr str; }; }; ###### implementation config = mkIf cfg.enable (let cfsslCertPathPrefix = "${config.services.cfssl.dataDir}/cfssl"; cfsslCert = "${cfsslCertPathPrefix}.pem"; cfsslKey = "${cfsslCertPathPrefix}-key.pem"; in { services.cfssl = mkIf (top.apiserver.enable) { enable = true; address = "0.0.0.0"; tlsCert = cfsslCert; tlsKey = cfsslKey; configFile = toString (pkgs.writeText "cfssl-config.json" (builtins.toJSON { signing = { profiles = { default = { usages = ["digital signature"]; auth_key = "default"; expiry = "720h"; }; }; }; auth_keys = { default = { type = "standard"; key = "file:${cfsslAPITokenPath}"; }; }; })); }; systemd.services.cfssl.preStart = with pkgs; with config.services.cfssl; mkIf (top.apiserver.enable) (concatStringsSep "\n" [ "set -e" (optionalString cfg.genCfsslCACert '' if [ ! -f "${cfg.caCertPathPrefix}.pem" ]; then ${cfssl}/bin/cfssl genkey -initca ${csrCA} | \ ${cfssl}/bin/cfssljson -bare ${cfg.caCertPathPrefix} fi '') (optionalString cfg.genCfsslAPICerts '' if [ ! -f "${dataDir}/cfssl.pem" ]; then ${cfssl}/bin/cfssl gencert -ca "${cfg.caCertPathPrefix}.pem" -ca-key "${cfg.caCertPathPrefix}-key.pem" ${csrCfssl} | \ ${cfssl}/bin/cfssljson -bare ${cfsslCertPathPrefix} fi '') (optionalString cfg.genCfsslAPIToken '' if [ ! -f "${cfsslAPITokenPath}" ]; then head -c ${toString (cfsslAPITokenLength / 2)} /dev/urandom | od -An -t x | tr -d ' ' >"${cfsslAPITokenPath}" fi chown cfssl "${cfsslAPITokenPath}" && chmod 400 "${cfsslAPITokenPath}" '')]); systemd.services.kube-certmgr-bootstrap = { description = "Kubernetes certmgr bootstrapper"; wantedBy = [ "certmgr.service" ]; after = [ "cfssl.target" ]; script = concatStringsSep "\n" ['' set -e # If there's a cfssl (cert issuer) running locally, then don't rely on user to # manually paste it in place. Just symlink. # otherwise, create the target file, ready for users to insert the token if [ -f "${cfsslAPITokenPath}" ]; then ln -fs "${cfsslAPITokenPath}" "${certmgrAPITokenPath}" else touch "${certmgrAPITokenPath}" && chmod 600 "${certmgrAPITokenPath}" fi '' (optionalString (cfg.pkiTrustOnBootstrap) '' if [ ! -f "${top.caFile}" ] || [ $(cat "${top.caFile}" | wc -c) -lt 1 ]; then ${pkgs.curl}/bin/curl --fail-early -f -kd '{}' ${remote}/api/v1/cfssl/info | \ ${pkgs.cfssl}/bin/cfssljson -stdout >${top.caFile} fi '') ]; serviceConfig = { RestartSec = "10s"; Restart = "on-failure"; }; }; services.certmgr = { enable = true; package = pkgs.certmgr-selfsigned; svcManager = "command"; specs = let mkSpec = _: cert: { inherit (cert) action; authority = { inherit remote; file.path = cert.caCert; root_ca = cert.caCert; profile = "default"; auth_key_file = certmgrAPITokenPath; }; certificate = { path = cert.cert; }; private_key = cert.privateKeyOptions; request = { inherit (cert) CN hosts; key = { algo = "rsa"; size = 2048; }; names = [ cert.fields ]; }; }; in mapAttrs mkSpec cfg.certs; }; #TODO: Get rid of kube-addon-manager in the future for the following reasons # - it is basically just a shell script wrapped around kubectl # - it assumes that it is clusterAdmin or can gain clusterAdmin rights through serviceAccount # - it is designed to be used with k8s system components only # - it would be better with a more Nix-oriented way of managing addons systemd.services.kube-addon-manager = mkIf top.addonManager.enable (mkMerge [{ environment.KUBECONFIG = with cfg.certs.addonManager; top.lib.mkKubeConfig "addon-manager" { server = top.apiserverAddress; certFile = cert; keyFile = key; }; } (optionalAttrs (top.addonManager.bootstrapAddons != {}) { serviceConfig.PermissionsStartOnly = true; preStart = with pkgs; let files = mapAttrsToList (n: v: writeText "${n}.json" (builtins.toJSON v)) top.addonManager.bootstrapAddons; in '' export KUBECONFIG=${clusterAdminKubeconfig} ${kubectl}/bin/kubectl apply -f ${concatStringsSep " \\\n -f " files} ''; })]); environment.etc.${cfg.etcClusterAdminKubeconfig}.source = mkIf (!isNull cfg.etcClusterAdminKubeconfig) clusterAdminKubeconfig; environment.systemPackages = mkIf (top.kubelet.enable || top.proxy.enable) [ (pkgs.writeScriptBin "nixos-kubernetes-node-join" '' set -e exec 1>&2 if [ $# -gt 0 ]; then echo "Usage: $(basename $0)" echo "" echo "No args. Apitoken must be provided on stdin." echo "To get the apitoken, execute: 'sudo cat ${certmgrAPITokenPath}' on the master node." exit 1 fi if [ $(id -u) != 0 ]; then echo "Run as root please." exit 1 fi read -r token if [ ''${#token} != ${toString cfsslAPITokenLength} ]; then echo "Token must be of length ${toString cfsslAPITokenLength}." exit 1 fi echo $token > ${certmgrAPITokenPath} chmod 600 ${certmgrAPITokenPath} echo "Restarting certmgr..." >&1 systemctl restart certmgr echo "Waiting for certs to appear..." >&1 ${optionalString top.kubelet.enable '' while [ ! -f ${cfg.certs.kubelet.cert} ]; do sleep 1; done echo "Restarting kubelet..." >&1 systemctl restart kubelet ''} ${optionalString top.proxy.enable '' while [ ! -f ${cfg.certs.kubeProxyClient.cert} ]; do sleep 1; done echo "Restarting kube-proxy..." >&1 systemctl restart kube-proxy ''} ${optionalString top.flannel.enable '' while [ ! -f ${cfg.certs.flannelClient.cert} ]; do sleep 1; done echo "Restarting flannel..." >&1 systemctl restart flannel ''} echo "Node joined succesfully" '')]; # isolate etcd on loopback at the master node # easyCerts doesn't support multimaster clusters anyway atm. services.etcd = with cfg.certs.etcd; { listenClientUrls = ["https://127.0.0.1:2379"]; listenPeerUrls = ["https://127.0.0.1:2380"]; advertiseClientUrls = ["https://etcd.local:2379"]; initialCluster = ["${top.masterAddress}=https://etcd.local:2380"]; initialAdvertisePeerUrls = ["https://etcd.local:2380"]; certFile = mkDefault cert; keyFile = mkDefault key; trustedCaFile = mkDefault caCert; }; networking.extraHosts = mkIf (config.services.etcd.enable) '' 127.0.0.1 etcd.${top.addons.dns.clusterDomain} etcd.local ''; services.flannel = with cfg.certs.flannelClient; { kubeconfig = top.lib.mkKubeConfig "flannel" { server = top.apiserverAddress; certFile = cert; keyFile = key; }; }; services.kubernetes = { apiserver = mkIf top.apiserver.enable (with cfg.certs.apiServer; { etcd = with cfg.certs.apiserverEtcdClient; { servers = ["https://etcd.local:2379"]; certFile = mkDefault cert; keyFile = mkDefault key; caFile = mkDefault caCert; }; clientCaFile = mkDefault caCert; tlsCertFile = mkDefault cert; tlsKeyFile = mkDefault key; serviceAccountKeyFile = mkDefault cfg.certs.serviceAccount.cert; kubeletClientCaFile = mkDefault caCert; kubeletClientCertFile = mkDefault cfg.certs.apiserverKubeletClient.cert; kubeletClientKeyFile = mkDefault cfg.certs.apiserverKubeletClient.key; proxyClientCertFile = mkDefault cfg.certs.apiserverProxyClient.cert; proxyClientKeyFile = mkDefault cfg.certs.apiserverProxyClient.key; }); controllerManager = mkIf top.controllerManager.enable { serviceAccountKeyFile = mkDefault cfg.certs.serviceAccount.key; rootCaFile = cfg.certs.controllerManagerClient.caCert; kubeconfig = with cfg.certs.controllerManagerClient; { certFile = mkDefault cert; keyFile = mkDefault key; }; }; scheduler = mkIf top.scheduler.enable { kubeconfig = with cfg.certs.schedulerClient; { certFile = mkDefault cert; keyFile = mkDefault key; }; }; kubelet = mkIf top.kubelet.enable { clientCaFile = mkDefault cfg.certs.kubelet.caCert; tlsCertFile = mkDefault cfg.certs.kubelet.cert; tlsKeyFile = mkDefault cfg.certs.kubelet.key; kubeconfig = with cfg.certs.kubeletClient; { certFile = mkDefault cert; keyFile = mkDefault key; }; }; proxy = mkIf top.proxy.enable { kubeconfig = with cfg.certs.kubeProxyClient; { certFile = mkDefault cert; keyFile = mkDefault key; }; }; }; }); }