├── .gitignore ├── LICENSE ├── README.md ├── config.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | release 3 | temp 4 | history 5 | remote-do-not-distribute 6 | .DS_Store 7 | /.idea 8 | node_modules 9 | /newrelic_agent.log 10 | /scripts/output 11 | /*.komodoproject 12 | *.pem 13 | *.key 14 | .otto 15 | .ottoid 16 | .vagrant 17 | npm-debug* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Issac Goldstand 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vault-pki-client 2 | 3 | ## Synopsis 4 | 5 | vault-pki-client is a tool, similar to [consul-template](https://github.com/hashicorp/consul-template) 6 | but crafted specifically for [Vault](https://vaultproject.io) and the 7 | [PKI (certificate) secret backend](https://vaultproject.io/docs/secrets/pki/index.html) 8 | 9 | The tool will connect to a Vault server and periodically request a x509 keypair, 10 | save the generated keypair to files, and optionally execute a command each time 11 | the files are updated. The tool runs as a daemon (unless the `--once` argument 12 | is given), and will continue to run in the background and update the keypair 13 | shortly before it expires. The idea is to enable system administrators to 14 | request shorter TTLs, aligning with Vault's principle of short-lived one-time 15 | secrets. 16 | 17 | ## Installation 18 | 19 | If you have node.js installed, simply `npm install -g vault-pki-client` 20 | 21 | If you don't have node.js installed, you can [download a binary package](https://github.com/issacg/vault-pki-client/releases/latest) 22 | 23 | ## Configuration 24 | 25 | ### Methods 26 | 27 | vault-pki-client makes use of the excellent [rc](https://github.com/dominictarr/rc) module, 28 | so variables can be passed as command line parameters, environment variables or 29 | provided via a configuration file named .vault-pki-clientrc 30 | 31 | For more detailed information about how to set parameters, see the [rc](https://github.com/dominictarr/rc#standards) page. 32 | 33 | ### Options 34 | 35 | | Parameter | Default Value | Description | 36 | |-----------|---------------|-------------| 37 | | `vault.server.address` | `http://127.0.0.1:8200` | The protocol, hostname and port of the Vault server. | 38 | | `vault.server.ca-cert` | None | Path to a PEM-encoded CA cert file to use to verify the Vault server SSL certificate | 39 | | `vault.server.ca-path` | None | Path to a directory of PEM-encoded CA cert files to verify the Vault server SSL certificate. | 40 | | `vault.server.tls-skip-verify` | `false` | If set, do not verify Vault's presented certificate before communicating with it. Setting this variable is not recommended except during testing. | 41 | | `vault.server.api-version` | `v1` | The API version to use when communicating with the Vault server. For now, only `v1` is supported. | 42 | | `vault.pki.path` | `pki` | The path to the requested pki mount point in the Vault server. | 43 | | `vault.pki.role` | None (**required**) | The name of the role used to request the client certificate pair. See [the Vault documentation](https://vaultproject.io/docs/secrets/pki/index.html) for details of how to configure this in the Vault server. | 44 | | `vault.token` | None | The token used to authenticate to the Vault server. | 45 | | `vault.token-renewable` | `false` | If `true`, vault-pki-client will attempt to renew the token based on the TTL of the token. The token will be renewed immediately on startup to determine the TTL. | 46 | | `certCN` | The hostname of the machine running vault-pki-client | The Common Name (CN) to be used in the requested x509 keypair. For example, `foo.example.com`. The value specified here must be a valid CN based on the role defined in `vault.pki.role` or the request will be rejected by the Vault server. | 47 | | `certAltNames` | `[]` | Subject Alternative Names to request in the cert. These are in addition to the value of `certCN`. Any values specified must be a valid CN based on the role defined in `vault.pki.role` or the entire request will be denied. | 48 | | `certIPs` | `[]` | IP Subject Alternative Name to request in the cert. The role defined in `vault.pki.role` must allow IP SANs or the entire request will be denied. | 49 | | `certTTL` | None | The TTL of the keypair being requested by the Vault server. In a production environment, this should normally be kept to a reasonably low value. See [the Vault documentation](https://vaultproject.io/docs/secrets/pki/index.html). If not specified, the Vault server will use the configured default lease TTL. Note that the value specified may not exceed the maximum TTL defined on the Vault server mount. | 50 | | `certFile` | `client.pem` | The file to store the x509 certificate returned by the Vault server. | 51 | | `keyFile` | `client.key` | The file to store the private key for the certificate returned by the Vault server. | 52 | | `caFile` | None | If specified, the file to store the certificate of the CA used to sign `certFile`. If empty, the CA certificate will not be written to disk. | 53 | | `onUpdate` | None | A command to run after updating the keypair. Can be used to restart services. For example `service httpd restart`. The command should exit quickly - if you want to start a service, don't start the service directly, but rather write a short-running script/batch file to start the service. | 54 | | `renewalCoefficient` | `0.9` | A coefficient applied to TTLs returned by the Vault server to determine when to renew secrets returned by the vault server. A value of `1.0` means 100% of the lease time, and will almost certainly mean that secrets will expire before they can be renewed. The default value of `0.9` means that secrets will be renewed at 90% of the TTL value. This affects token renewel (if `vault.token-renewable` is set to `true`) and `certTTL` | 55 | | `once` | `false` | If `true`, specifies that vault-pki-client will request a new keypair (including writing to disk and execuring `onUpdate`) and exit immediately without staying alive to renew the keypair when `certTTL` expires` | 56 | 57 | ###Vault environment variables 58 | 59 | vault-pki-client will utilize the [standard Vault environment variables](https://vaultproject.io/docs/commands/environment.html) if they are defined. 60 | Options passed directly to vault-pki-client (including by environment variables) will take precedence over these environment variables, but these environment 61 | variables will take precedence over the defaults spefified in the Options section above. 62 | 63 | For example if the environment variable `VAULT_TOKEN` is set to `foo`, and no 64 | value for `vault.token` is specified, vault-pki-client will use `foo` as the 65 | Vault token. However, if the environment variable `VAULT_TOKEN` is set to 66 | `foo`, and the environment variable `vault-pki-client_vault__token` is set to 67 | `bar`, vault-pki-client will use `bar` as the Vault token. 68 | 69 | ## Examples 70 | 71 | For more information about configuration files, environment variables and arguments, 72 | see the configuration section. 73 | 74 | ``` vault-pki-client --vault.pki.role=example.com --certFile=client.pem --keyFile=client.key --caFile=ca.pem --certCN=foo.example.com --certTTL=24h ``` 75 | 76 | This example will attempt to fetch an x509 keypair with the CN `foo.example.com` 77 | which expires 24 hours in the future. 78 | 79 | ## License 80 | 81 | Copyright 2015 Issac Goldstand 82 | 83 | Licensed under the Apache License, Version 2.0 (the "License"); 84 | you may not use this file except in compliance with the License. 85 | You may obtain a copy of the License at 86 | 87 | http://www.apache.org/licenses/LICENSE-2.0 88 | 89 | Unless required by applicable law or agreed to in writing, software 90 | distributed under the License is distributed on an "AS IS" BASIS, 91 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 92 | See the License for the specific language governing permissions and 93 | limitations under the License. 94 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var rc = require("rc"); 2 | 3 | var defaults = { 4 | vault: { 5 | server: { 6 | "address": process.env.VAULT_ADDR || "http://localhost:8200", 7 | "ca-cert": process.env.VAULT_CACERT || undefined, 8 | "ca-path": process.env.VAULT_CAPATH || undefined, 9 | "tls-skip-verify": process.env.VAULT_SKIP_VERIFY || false, 10 | "api-version": "v1" 11 | }, pki: { 12 | "path": "pki", 13 | "role": "" 14 | }, 15 | "token": process.env.VAULT_TOKEN || "", 16 | "token-renewable": false 17 | }, 18 | certCN: require("os").hostname(), 19 | certAltNames: [], 20 | certIPs: [], 21 | certTTL: undefined, 22 | certFile: "client.pem", 23 | keyFile: "client.key", 24 | caFile: undefined, 25 | onUpdate: undefined, 26 | renewalCoefficient: 0.9, 27 | once: false 28 | }; 29 | 30 | module.exports = rc("vault-pki-client", defaults); 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var debug = require('debug')('vault-pki-client:main'), 4 | config = require('./config'), 5 | Promise = require('bluebird'), 6 | util = require('util'), 7 | request = require('request-promise'), 8 | pkginfo = require('./package.json'), 9 | child = Promise.promisifyAll(require('child_process')), 10 | fs = Promise.promisifyAll(require('fs')), 11 | MAX_TIMEOUT = 0x7FFFFFFF; // http://nodejs.org/api/all.html#all_settimeout_cb_ms 12 | 13 | var token = config.vault.token; 14 | function main() { 15 | if (config.version) { 16 | console.log(pkginfo.name + " v" + pkginfo.version); 17 | process.exit(0); 18 | } 19 | Promise.resolve() 20 | .then(config.vault["token-renewable"] ? renewVaultToken : Promise.resolve) 21 | .then(fetchCert) 22 | .catch(function(e) { 23 | if (e && e.name && e.name == "StatusCodeError") { 24 | console.error("Vault error: " + e.statusCode + " " + e.error.errors.join(" ")); 25 | } else { 26 | console.error(e); 27 | } 28 | }); 29 | } 30 | 31 | var fetchCert = (function() { 32 | var debug = require('debug')('vault-pki-client:certRenewal'); 33 | var exec, args = []; 34 | if (config.onUpdate) { 35 | args = config.onUpdate.split(/\s+/); 36 | exec = args.shift(); 37 | } 38 | return function() { 39 | var path = [config.vault.pki.path, "issue", config.vault.pki.role].join("/"); 40 | debug("Attempting to fetch a keypair from " + path); 41 | var opts = { 42 | common_name: config.certCN, 43 | alt_names: config.certAltNames.join(","), 44 | ip_sans: config.certIPs.join(",") 45 | } 46 | if (config.certTTL) opts.ttl = config.certTTL; 47 | return vaultRequest(path, 'POST', opts).then(function(data) { 48 | return Promise.all([ 49 | saveKey(data.data.private_key), 50 | saveCert(data.data.certificate), 51 | saveCA(data.data.issuing_ca) 52 | ]).then(function() { 53 | if (config.once) return Promise.resolve(); 54 | var next = data.lease_duration * config.renewalCoefficient * 1000; 55 | if (next > MAX_TIMEOUT) { 56 | debug("Renewal of " + next + "ms is longer than max timer of " + MAX_TIMEOUT + "ms. Truncating"); 57 | next = MAX_TIMEOUT; 58 | } 59 | debug("Next renewal in " + next + "ms"); 60 | setTimeout(fetchCert, next); 61 | }).then(function() { 62 | if (config.onUpdate) { 63 | debug("Executing " + exec + " " + args.join(" ")); 64 | return child.spawnAsync(exec, args, {}); 65 | } else { 66 | return Promise.resolve(); 67 | } 68 | }).catch(function(e) { 69 | debug("Failed to fetch and update keypair"); 70 | console.error(e); 71 | }); 72 | }); 73 | }; 74 | function saveKey(data) { 75 | debug("Writing private key to " + config.keyFile); 76 | return fs.writeFileAsync(config.keyFile, data); 77 | } 78 | 79 | function saveCert(data) { 80 | debug("Writing certificate to " + config.certFile); 81 | return fs.writeFileAsync(config.certFile, data); 82 | } 83 | 84 | function saveCA(data) { 85 | if (!config.caFile) return Promise.resolve(); 86 | debug("Writing CA certificate to " + config.caFile); 87 | return fs.writeFileAsync(config.caFile, data); 88 | } 89 | })(); 90 | 91 | // Attempt to periodically renew the vault token 92 | var renewVaultToken = (function() { 93 | var debug = require('debug')('vault-pki-client:tokenRenewal'); 94 | return function() { 95 | debug("Attempting to renew vault token"); 96 | return vaultRequest('auth/token/renew-self','POST').then(function(data) { 97 | token = data.auth.client_token; 98 | debug("Token renewal succeeded"); 99 | if (data.auth.renewable) { 100 | var next = data.auth.lease_duration * config.renewalCoefficient * 1000; 101 | if (next > MAX_TIMEOUT) { 102 | debug("Renewal of " + next + "ms is longer than max timer of " + MAX_TIMEOUT + "ms. Truncating"); 103 | next = MAX_TIMEOUT; 104 | } 105 | debug("Next renewal in " + next + "ms"); 106 | setTimeout(renewVaultToken, next).unref(); 107 | } 108 | return Promise.resolve(); 109 | }).catch(function(err) { 110 | debug("Token renewal failed"); 111 | console.error(err); 112 | return Promise.reject(); 113 | }); 114 | }; 115 | })(); 116 | 117 | // Build request options 118 | function buildReqOpts() { 119 | var reqOpts = { 120 | ca:[], 121 | rejectUnauthorized: !config.vault.server["tls-skip-verify"], 122 | followAllRedirects: false 123 | }; 124 | 125 | if (config.vault.server["ca-path"]) { 126 | // This won't work for bundle files with multiple CAs in a single file... 127 | var match = "-----BEGIN CERTIFICATE-----", 128 | len = match.length; 129 | fs.readdirSync(config.vault.server["ca-path"]).forEach(function(file) { 130 | file = [config.vault.server["ca-path"], file].join("/"); 131 | if (!fs.statSync(file).isFile()) return; 132 | var buf = fs.readFileSync(file); 133 | if (buf.slice(0, len) == match) 134 | reqOpts.ca.push(buf); 135 | }) 136 | } 137 | 138 | if (config.vault.server["ca-cert"]) 139 | reqOpts.ca.push(fs.readFileSync(config.vault.server["ca-cert"])); 140 | 141 | // Fallback to default node-bundled CAs 142 | if (reqOpts.ca.length == 0) 143 | delete reqOpts.ca; 144 | 145 | if (config.vault.server["client-cert"]) 146 | reqOpts.ca.push(fs.readFileSync(config.vault.server["client-cert"])); 147 | 148 | if (config.vault.server["client-key"]) 149 | reqOpts.ca.push(fs.readFileSync(config.vault.server["ca-cert"])); 150 | 151 | return reqOpts; 152 | } 153 | 154 | var vaultRequest = (function () { 155 | var debug = require('debug')('vault-pki-client:http'); 156 | var defOpts = buildReqOpts(); 157 | return function(url, method, body) { 158 | method = method || "GET"; 159 | body = body || undefined; 160 | var opts = util._extend(defOpts, { 161 | url: [config.vault.server['address'], config.vault.server['api-version'], url].join("/"), 162 | headers: { 163 | "X-Vault-Token": token 164 | }, 165 | method: method, 166 | body: body, 167 | json: true 168 | }); 169 | debug(method + "ing data to " + opts.url); 170 | return request(opts).then(function(data) {debug("Got data: " + data); return data}); 171 | }; 172 | })(); 173 | 174 | main(); 175 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vault-pki-client", 3 | "version": "1.0.2", 4 | "description": "Tool to manage a keypair provided by HashiCorp Vault", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "nexe": "nexe" 9 | }, 10 | "bin": { 11 | "vault-pki-client": "index.js" 12 | }, 13 | "author": "Issac Goldstand ", 14 | "license": "Apache-2.0", 15 | "dependencies": { 16 | "bluebird": "^3.1.1", 17 | "debug": "^2.2.0", 18 | "rc": "^1.1.5", 19 | "request-promise": "^1.0.2" 20 | }, 21 | "devDependencies": { 22 | "nexe": "^0.4.2" 23 | }, 24 | "nexe": { 25 | "input": "index.js", 26 | "output": "vault-pki-client^$", 27 | "temp": "./temp/src", 28 | "runtime": { 29 | "framework": "nodejs", 30 | "version": "4.2.4", 31 | "ignoreFlags": true, 32 | "nodeConfigureArgs": ["--fully-static"], 33 | "nodeMakeArgs": ["-j","4"] 34 | } 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/issacg/vault-pki-client.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/issacg/vault-pki-client/issues" 42 | }, 43 | "homepage": "https://github.com/issacg/vault-pki-client#readme" 44 | } 45 | --------------------------------------------------------------------------------