├── .babelrc ├── .gitignore ├── Dockerfile ├── Dockerfile.test ├── LICENSE ├── README.md ├── circle.yml ├── default.conf ├── default.crt ├── docker-cloud-watch ├── docker-cloud-watch.js ├── docker-compose.yml ├── lib ├── etcd-watch.js ├── nginx-template.js └── reload-nginx.js ├── nginx.conf ├── package.json └── test ├── etcd-watch.test.js ├── functional.js └── mocks ├── api.com.crt ├── test2.com.crt ├── test3.com.crt ├── test5.com.crt └── test6.com.crt /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | certs 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | RUN apt-get update 4 | RUN apt-get install nginx curl -y 5 | RUN rm /etc/nginx/sites-enabled/default 6 | # gpg keys listed at https://github.com/nodejs/node 7 | RUN set -ex \ 8 | && for key in \ 9 | 9554F04D7259F04124DE6B476D5A82AC7E37093B \ 10 | 94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \ 11 | 0034A06D9D9B0064CE8ADF6BF1747F4AD2306D93 \ 12 | FD3A5288F042B6850C66B31F09FE44734EB7990E \ 13 | 71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \ 14 | DD8F2338BAE7501E3DD5AC78C273792F7D83545D \ 15 | ; do \ 16 | gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; \ 17 | done 18 | 19 | ENV NPM_CONFIG_LOGLEVEL info 20 | ENV NODE_VERSION 5.1.1 21 | 22 | RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz" \ 23 | && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ 24 | && gpg --verify SHASUMS256.txt.asc \ 25 | && grep " node-v$NODE_VERSION-linux-x64.tar.gz\$" SHASUMS256.txt.asc | sha256sum -c - \ 26 | && tar -xzf "node-v$NODE_VERSION-linux-x64.tar.gz" -C /usr/local --strip-components=1 \ 27 | && rm "node-v$NODE_VERSION-linux-x64.tar.gz" SHASUMS256.txt.asc 28 | 29 | WORKDIR /app 30 | COPY ./package.json /app/package.json 31 | RUN npm install --production 32 | 33 | COPY ./.babelrc /app/.babelrc 34 | COPY ./docker-cloud-watch.js /app/docker-cloud-watch.js 35 | COPY ./nginx.conf /etc/nginx/nginx.conf 36 | COPY ./docker-cloud-watch /usr/local/bin/docker-cloud-watch 37 | RUN chmod +x /usr/local/bin/docker-cloud-watch 38 | COPY ./lib /app/lib 39 | COPY ./default.crt /certs/default.crt 40 | 41 | EXPOSE 80 42 | 43 | CMD ["docker-cloud-watch"] 44 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | RUN apt-get update 4 | RUN apt-get install nginx curl -y 5 | RUN rm /etc/nginx/sites-enabled/default 6 | # gpg keys listed at https://github.com/nodejs/node 7 | RUN set -ex \ 8 | && for key in \ 9 | 9554F04D7259F04124DE6B476D5A82AC7E37093B \ 10 | 94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \ 11 | 0034A06D9D9B0064CE8ADF6BF1747F4AD2306D93 \ 12 | FD3A5288F042B6850C66B31F09FE44734EB7990E \ 13 | 71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \ 14 | DD8F2338BAE7501E3DD5AC78C273792F7D83545D \ 15 | ; do \ 16 | gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; \ 17 | done 18 | 19 | ENV NPM_CONFIG_LOGLEVEL info 20 | ENV NODE_VERSION 5.1.1 21 | 22 | RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz" \ 23 | && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ 24 | && gpg --verify SHASUMS256.txt.asc \ 25 | && grep " node-v$NODE_VERSION-linux-x64.tar.gz\$" SHASUMS256.txt.asc | sha256sum -c - \ 26 | && tar -xzf "node-v$NODE_VERSION-linux-x64.tar.gz" -C /usr/local --strip-components=1 \ 27 | && rm "node-v$NODE_VERSION-linux-x64.tar.gz" SHASUMS256.txt.asc 28 | 29 | WORKDIR /app 30 | COPY ./package.json /app/package.json 31 | RUN npm install 32 | 33 | COPY ./.babelrc /app/.babelrc 34 | COPY ./docker-cloud-watch.js /app/docker-cloud-watch.js 35 | COPY ./nginx.conf /etc/nginx/nginx.conf 36 | COPY ./docker-cloud-watch /usr/local/bin/docker-cloud-watch 37 | RUN chmod +x /usr/local/bin/docker-cloud-watch 38 | COPY ./lib /app/lib 39 | COPY ./test /app/test 40 | COPY ./default.crt /certs/default.crt 41 | 42 | EXPOSE 80 43 | 44 | CMD ["npm", "run", "mocha"] 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Will Stern 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nginx-etcd 2 | Dynamic Nginx Load Balancing for Docker 3 | 4 | * run etcd 5 | * run one or more copies of this container to act as public entry points/load balancers to your cluster 6 | * register services to etcd 7 | * everything works! 8 | - Nginx notices services and generates a new config 9 | - it will save ssl certs and use virtual hosts to direct traffic from multiple domains 10 | * see the [docker-compose.yml](https://github.com/willrstern/nginx-etcd/blob/master/docker-compose.yml) file for a full example of the containers needed along with some sample web containers 11 | 12 | ## Configure Via ENV Vars 13 | * `NGINX_NAME` (required) - so services can determine which nginx lb will balance their traffic 14 | * `NGINX_ETCD_HOST` default `etcd` 15 | * `NGINX_REFRESH` default 5000 - rate at which it refreshes from etcd 16 | * `NGINX_DEBUG` enable lots of logging output 17 | * `SLACK_WEBHOOK` optionally shout on a slack channel when a templated config fails to reload (nginx will keep running with last-good-config). If for some reason, a service manages create a bad config, service discovery will be frozen until the bad registration is removed from ETCD. 18 | 19 | ## Expected ETCD structure: 20 | Services should register in the following format: 21 | ```yaml 22 | /v2/keys/services: 23 | web: 24 | tags: 25 | nginx: 'primary' #corresponds to NGINX_NAME 26 | hosts: 27 | test.com: 28 | ssl: true #(optional) 29 | #(optional) combined .key and .crt file replacing line breaks with \n 30 | cert: "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEA..." 31 | upstream: 32 | 1b9d3522da76: '123.45.67.8:80' 33 | c7c508e915ed: '123.45.67.9:80' 34 | test2.com: 35 | upstream: 36 | 1b9d3522da76: '123.45.67.8:80' 37 | c7c508e915ed: '123.45.67.9:80' 38 | api: 39 | tags: 40 | nginx: 'primary' 41 | hosts: 42 | api.com: 43 | upstream: 44 | 1abc3ab1c33: '123.45.67.10:3000' 45 | 7dacb15ba5b: '123.45.67.11:3000' 46 | ``` 47 | * You can now point test.com, test2.com & api.com DNS to the nginx instances 48 | * When the `Host` header is `api.com`, api upstreams will be served, `test.com` will serve `web` upstreams, etc 49 | 50 | ## SSL Termination 51 | - create a cert 52 | - concatenate the `.key` and `.crt` files 53 | - replace newlines with `\n` and copy the output 54 | - add the combined key & cert into services//host//cert in etcd 55 | - [see here for an example](https://github.com/willrstern/nginx-etcd/blob/master/docker-compose.yml#L45) 56 | 57 | ## Slack Integration 58 | - Before reloading a config, it runs `nginx -t` to make sure it is valid 59 | - If a config fails, it will continue using the last-good-config until a working config is generated 60 | - Add `SLACK_WEBHOOK=https://hooks.slack.com/services/T02RK...` env var to get notifications when a config fails. 61 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | services: 3 | - docker 4 | 5 | dependencies: 6 | override: 7 | - docker-compose build test 8 | - docker-compose up -d 9 | 10 | test: 11 | override: 12 | - docker-compose run test 13 | 14 | deployment: 15 | master: 16 | branch: master 17 | commands: 18 | - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS 19 | - npm run deploy 20 | alpha: 21 | branch: alpha 22 | commands: 23 | - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS 24 | - npm run deploy:alpha 25 | -------------------------------------------------------------------------------- /default.conf: -------------------------------------------------------------------------------- 1 | 2 | # default server 3 | server { 4 | listen 80 default_server; 5 | listen 443 ssl default_server; 6 | 7 | server_name _; 8 | 9 | ssl_certificate /certs/default.crt; 10 | ssl_certificate_key /certs/default.crt; 11 | 12 | location / { 13 | return 404; 14 | } 15 | 16 | location /health { 17 | add_header Content-Type text/html; 18 | return 200 "healthy"; 19 | } 20 | } 21 | 22 | # status for stats such as datadog 23 | server { 24 | listen 81; 25 | server_name localhost; 26 | 27 | access_log off; 28 | #allow 127.0.0.1; 29 | #deny all; 30 | 31 | location /nginx_status { 32 | stub_status on; 33 | } 34 | } 35 | 36 | # test.com 37 | upstream test_com { 38 | server 192.168.34.12:80; 39 | server 192.168.34.12:80; 40 | } 41 | 42 | server { 43 | listen 80; 44 | 45 | server_name test.com; 46 | 47 | location / { 48 | proxy_pass http://test_com; 49 | proxy_set_header Host $host; 50 | proxy_set_header X-Forwarded-For $remote_addr; 51 | } 52 | } 53 | 54 | server { 55 | listen 443 ssl; 56 | server_name test.com; 57 | 58 | ssl_certificate /certs/test.com.crt; 59 | ssl_certificate_key /certs/test.com.crt; 60 | 61 | ssl_session_cache shared:SSL:20m; 62 | ssl_session_timeout 10m; 63 | 64 | ssl_prefer_server_ciphers on; 65 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 66 | ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS; 67 | 68 | add_header Strict-Transport-Security "max-age=31536000"; 69 | 70 | location / { 71 | proxy_pass http://test_com; 72 | proxy_set_header Host $host; 73 | proxy_set_header X-Forwarded-For $remote_addr; 74 | proxy_set_header X-Forwarded-Proto $scheme; 75 | } 76 | } 77 | # test2.com 78 | upstream test_2_com { 79 | server 192.168.34.12:80; 80 | server 192.168.34.12:80; 81 | } 82 | 83 | server { 84 | listen 80; 85 | 86 | server_name test2.com; 87 | 88 | location / { 89 | proxy_pass http://test_2_com; 90 | proxy_set_header Host $host; 91 | proxy_set_header X-Forwarded-For $remote_addr; 92 | } 93 | } 94 | 95 | server { 96 | listen 443 ssl; 97 | server_name test2.com; 98 | 99 | ssl_certificate /certs/test2.com.crt; 100 | ssl_certificate_key /certs/test2.com.crt; 101 | 102 | ssl_session_cache shared:SSL:20m; 103 | ssl_session_timeout 10m; 104 | 105 | ssl_prefer_server_ciphers on; 106 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 107 | ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS; 108 | 109 | add_header Strict-Transport-Security "max-age=31536000"; 110 | 111 | location / { 112 | proxy_pass http://test_2_com; 113 | proxy_set_header Host $host; 114 | proxy_set_header X-Forwarded-For $remote_addr; 115 | proxy_set_header X-Forwarded-Proto $scheme; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /default.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQC/G6eud0cQqqI729F+uxT1Zv1HLpqz/qOc/3GjtZohNUyOCJK0 3 | goiiNwz9nNy62Q3iAknW/EP6TKNfiN1AFNsqECsxOku0mUAyvERLeBfWpXVXTi27 4 | 7ml7KXoVhhDxPj3/PDsgyvrEsQ8e9/jDvkzovSKyfMJa0shbi6gwzJr+awIDAQAB 5 | AoGBAK0yoBZzDVnieyOqxcOIQ6dgjlzrtNM6DQglTdVjqWs9RcNXq7Wis7foEoLq 6 | nfVM79ML5eXMPMNkn4/elz4TaMe1tQKzeevy7waLEjLDlrtqQs4duX4ulUhQvDr2 7 | ZnWLiaoGIN/K+QnHzR1k7Kj07sT8PL3gIwqtqRdDBSO4ljIRAkEA6wMZ/OxKJCH6 8 | lKQ3C+7wlXttmBhkGQpzjvC0E8yJFR2ui/SCnvL75OD5+2pDUGpoXaIeCXDAisWN 9 | Ai/KZ9bF2QJBANAszwEUeU1cjf6vPTxrLOzcVLZSdgr3NBe8IAUtE46jL3rdfWlQ 10 | AYPmXfmRynLk8hZM3LgJFM1gO8JrFTJBp+MCQE3UUR76AfPFbP8dAz3oe7SFk93y 11 | 9fN1CqAkBv8nlZ5wngWrjDansdQyzZb9sh1HoBiiP+BQfvN2SSSYPyf0cMECQQC2 12 | kQV92fnDyc7Rs9eNXCTLGTPFra3OUhvCUP736x9CsYRbSVHKARtDFM4HqD8W4ggZ 13 | XJEZaQVwU9w01fqB16inAkBW5FWZixHCqHrDHMKevB+VRGtr94s0yOAxUZPPEAT/ 14 | wIBs4GGlzYk6sPgp8vMOHCtoox2JjzzgiZCsU2HObLS/ 15 | -----END RSA PRIVATE KEY----- 16 | -----BEGIN CERTIFICATE----- 17 | MIIB8TCCAVoCCQCrK/E7Wz0CxTANBgkqhkiG9w0BAQUFADA9MQswCQYDVQQGEwJV 18 | UzELMAkGA1UECBMCVFgxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 19 | ZDAeFw0xNjAyMjMxNzUxMzBaFw0xNzAyMjIxNzUxMzBaMD0xCzAJBgNVBAYTAlVT 20 | MQswCQYDVQQIEwJUWDEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk 21 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/G6eud0cQqqI729F+uxT1Zv1H 22 | Lpqz/qOc/3GjtZohNUyOCJK0goiiNwz9nNy62Q3iAknW/EP6TKNfiN1AFNsqECsx 23 | Oku0mUAyvERLeBfWpXVXTi277ml7KXoVhhDxPj3/PDsgyvrEsQ8e9/jDvkzovSKy 24 | fMJa0shbi6gwzJr+awIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAAQG+EhyuEQleHRg 25 | uZZnGKIYbeODAWTY4UOVNjV2AItHWk/yPDbPoxhj9e1iC7JdKHgJTLaJw0JzLuXx 26 | mPvyczvXfORsTv0Isc3JH71xhZ2GLX10rhQKBzIzud6CwopFmfdAAM0/z4gJ67JZ 27 | oAPJD828GizKayML5BIu3hQSufiy 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /docker-cloud-watch: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | # Start the Nginx service using the generated config 6 | echo "[nginx] starting nginx service..." 7 | service nginx start 8 | 9 | #command to: 10 | # - watch etcd 11 | # - on changes, create a new config from a template 12 | # - test new config with "nginx -t" 13 | # - if new config passes, run "service nginx reload" 14 | node docker-cloud-watch 15 | 16 | # Follow the logs to allow the script to continue running 17 | tail -f /var/log/nginx/*.log 18 | -------------------------------------------------------------------------------- /docker-cloud-watch.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | 3 | var getServices = require('./lib/etcd-watch').default 4 | var reloadNginx = require("./lib/reload-nginx").default 5 | 6 | getServices(reloadNginx) 7 | 8 | setInterval(function() { 9 | getServices(reloadNginx) 10 | }, process.env.NGINX_REFRESH || 5000) 11 | 12 | console.log("Watching Etcd for Changes") 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | test: 2 | build: . 3 | dockerfile: Dockerfile.test 4 | environment: 5 | - NGINX_NAME=primary 6 | links: 7 | - etcd 8 | - web 9 | - web2 10 | - api 11 | ports: 12 | - 80:80 13 | etcd: 14 | image: elcolio/etcd 15 | registrator: 16 | image: willrstern/dockercloud-etcd-registrator 17 | links: 18 | - etcd:etcd 19 | volumes: 20 | - /var/run/docker.sock:/var/run/docker.sock 21 | web: 22 | image: dockercloud/hello-world 23 | environment: 24 | - "DOCKERCLOUD_IP_ADDRESS=web" 25 | - "SERVICE_PORT=80" 26 | - "SERVICE_NAME=web" 27 | - "SERVICE_VIRTUAL_HOSTS=test.com,test2.com" 28 | - "SERVICE_TAGS=nginx:primary" 29 | web2: 30 | image: dockercloud/hello-world 31 | environment: 32 | - "DOCKERCLOUD_IP_ADDRESS=web2" 33 | - "SERVICE_PORT=80" 34 | - "SERVICE_NAME=web" 35 | - "SERVICE_VIRTUAL_HOSTS=test.com,test2.com" 36 | - "SERVICE_TAGS=foo:bar,nginx:primary" 37 | api: 38 | image: dockercloud/hello-world 39 | environment: 40 | - "DOCKERCLOUD_IP_ADDRESS=api" 41 | - "SERVICE_PORT=80" 42 | - "SERVICE_NAME=api" 43 | - "SERVICE_VIRTUAL_HOSTS=api.com" 44 | - "SERVICE_TAGS=nginx:primary" 45 | - "SERVICE_CERTS=-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQC/G6eud0cQqqI729F+uxT1Zv1HLpqz/qOc/3GjtZohNUyOCJK0\ngoiiNwz9nNy62Q3iAknW/EP6TKNfiN1AFNsqECsxOku0mUAyvERLeBfWpXVXTi27\n7ml7KXoVhhDxPj3/PDsgyvrEsQ8e9/jDvkzovSKyfMJa0shbi6gwzJr+awIDAQAB\nAoGBAK0yoBZzDVnieyOqxcOIQ6dgjlzrtNM6DQglTdVjqWs9RcNXq7Wis7foEoLq\nnfVM79ML5eXMPMNkn4/elz4TaMe1tQKzeevy7waLEjLDlrtqQs4duX4ulUhQvDr2\nZnWLiaoGIN/K+QnHzR1k7Kj07sT8PL3gIwqtqRdDBSO4ljIRAkEA6wMZ/OxKJCH6\nlKQ3C+7wlXttmBhkGQpzjvC0E8yJFR2ui/SCnvL75OD5+2pDUGpoXaIeCXDAisWN\nAi/KZ9bF2QJBANAszwEUeU1cjf6vPTxrLOzcVLZSdgr3NBe8IAUtE46jL3rdfWlQ\nAYPmXfmRynLk8hZM3LgJFM1gO8JrFTJBp+MCQE3UUR76AfPFbP8dAz3oe7SFk93y\n9fN1CqAkBv8nlZ5wngWrjDansdQyzZb9sh1HoBiiP+BQfvN2SSSYPyf0cMECQQC2\nkQV92fnDyc7Rs9eNXCTLGTPFra3OUhvCUP736x9CsYRbSVHKARtDFM4HqD8W4ggZ\nXJEZaQVwU9w01fqB16inAkBW5FWZixHCqHrDHMKevB+VRGtr94s0yOAxUZPPEAT/\nwIBs4GGlzYk6sPgp8vMOHCtoox2JjzzgiZCsU2HObLS/\n-----END RSA PRIVATE KEY-----\n-----BEGIN CERTIFICATE-----\nMIIB8TCCAVoCCQCrK/E7Wz0CxTANBgkqhkiG9w0BAQUFADA9MQswCQYDVQQGEwJV\nUzELMAkGA1UECBMCVFgxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0\nZDAeFw0xNjAyMjMxNzUxMzBaFw0xNzAyMjIxNzUxMzBaMD0xCzAJBgNVBAYTAlVT\nMQswCQYDVQQIEwJUWDEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/G6eud0cQqqI729F+uxT1Zv1H\nLpqz/qOc/3GjtZohNUyOCJK0goiiNwz9nNy62Q3iAknW/EP6TKNfiN1AFNsqECsx\nOku0mUAyvERLeBfWpXVXTi277ml7KXoVhhDxPj3/PDsgyvrEsQ8e9/jDvkzovSKy\nfMJa0shbi6gwzJr+awIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAAQG+EhyuEQleHRg\nuZZnGKIYbeODAWTY4UOVNjV2AItHWk/yPDbPoxhj9e1iC7JdKHgJTLaJw0JzLuXx\nmPvyczvXfORsTv0Isc3JH71xhZ2GLX10rhQKBzIzud6CwopFmfdAAM0/z4gJ67JZ\noAPJD828GizKayML5BIu3hQSufiy\n-----END CERTIFICATE-----" 46 | -------------------------------------------------------------------------------- /lib/etcd-watch.js: -------------------------------------------------------------------------------- 1 | import Etcd from "node-etcd" 2 | import flatten from "etcd-flatten" 3 | import set from "object-set" 4 | 5 | const { NGINX_ETCD_HOST } = process.env 6 | const etcd = new Etcd(NGINX_ETCD_HOST || "etcd") 7 | 8 | export default function getServices(cb) { 9 | etcd.get("services", {recursive: true}, (err, response) => { 10 | if (err) { 11 | return console.log("Error fetching services from Etcd", err); 12 | } 13 | const services = parseServices(response) 14 | cb(services); 15 | }) 16 | } 17 | 18 | function parseServices(response) { 19 | const services = {} 20 | const flattened = flatten(response.node) 21 | 22 | //replace dots with something unliekly to monkey patch hostnames e.g. test.com => test*^*com 23 | Object.keys(flattened).forEach((key) => { 24 | const dotKey = key.replace(/\./g, "*^*").replace(/\//g, ".") 25 | set(services, dotKey, flattened[key]) 26 | }) 27 | 28 | //convert upstreams from [{id: upstream}], to [upstream] 29 | Object.keys(services.services) 30 | .forEach((serviceName) => { 31 | const service = services.services[serviceName] 32 | if (service.hosts) { 33 | Object.keys(service.hosts) 34 | .forEach((key) => { 35 | const host = service.hosts[key] 36 | const upstream = [] 37 | 38 | Object.keys(host.upstream).forEach((key) => { 39 | upstream.push(host.upstream[key]) 40 | }) 41 | 42 | host.upstream = upstream; 43 | }) 44 | } 45 | }) 46 | 47 | return services.services; 48 | } 49 | -------------------------------------------------------------------------------- /lib/nginx-template.js: -------------------------------------------------------------------------------- 1 | import hogan from "hogan.js" 2 | 3 | export default hogan.compile(` 4 | # default server 5 | server { 6 | listen 80 default_server; 7 | listen 443 ssl default_server; 8 | 9 | server_name _; 10 | 11 | ssl_certificate /certs/default.crt; 12 | ssl_certificate_key /certs/default.crt; 13 | 14 | location / { 15 | return 404; 16 | } 17 | 18 | location /health { 19 | add_header Content-Type text/html; 20 | return 200 "healthy"; 21 | } 22 | } 23 | 24 | # status for stats such as datadog 25 | server { 26 | listen 81; 27 | server_name localhost; 28 | 29 | access_log off; 30 | #allow 127.0.0.1; 31 | #deny all; 32 | 33 | location /nginx_status { 34 | stub_status on; 35 | } 36 | } 37 | 38 | {{#configs}} 39 | # {{host}} 40 | upstream {{upstreamName}} { 41 | {{#upstream}} 42 | server {{.}}; 43 | {{/upstream}} 44 | } 45 | 46 | server { 47 | listen 80; 48 | 49 | server_name {{host}}; 50 | 51 | location / { 52 | proxy_pass http://{{upstreamName}}; 53 | proxy_set_header Host $host; 54 | proxy_set_header X-Forwarded-For $remote_addr; 55 | } 56 | } 57 | 58 | {{#ssl}} 59 | server { 60 | listen 443 ssl; 61 | server_name {{host}}; 62 | 63 | ssl_certificate /certs/{{host}}.crt; 64 | ssl_certificate_key /certs/{{host}}.crt; 65 | 66 | ssl_session_cache shared:SSL:20m; 67 | ssl_session_timeout 10m; 68 | 69 | ssl_prefer_server_ciphers on; 70 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 71 | ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS; 72 | 73 | add_header Strict-Transport-Security "max-age=31536000"; 74 | 75 | location / { 76 | proxy_pass http://{{upstreamName}}; 77 | proxy_set_header Host $host; 78 | proxy_set_header X-Forwarded-For $remote_addr; 79 | proxy_set_header X-Forwarded-Proto $scheme; 80 | } 81 | } 82 | {{/ssl}} 83 | {{/configs}} 84 | `) 85 | -------------------------------------------------------------------------------- /lib/reload-nginx.js: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import checksum from "checksum" 3 | import fs from "fs" 4 | import { execSync } from "child_process" 5 | import { find, intersection, sortBy, snakeCase, trim } from "lodash" 6 | 7 | import nginxTemplate from "./nginx-template" 8 | 9 | const { NGINX_DEBUG, NGINX_NAME, SLACK_WEBHOOK: slackWebhook } = process.env 10 | const configFileName = process.env.NGINX_CONFIG_FILE || "/etc/nginx/conf.d/default.conf" 11 | const certsPath = process.env.NGINX_CERTS || "/certs" 12 | 13 | try { 14 | fs.mkdirSync(certsPath) 15 | } catch(e) {} 16 | 17 | /*** 18 | RUN 19 | ***/ 20 | export default function(services) { 21 | const configs = parseServices(services) 22 | 23 | if (configs.length) { 24 | const newNginxConf = nginxTemplate.render({ configs }) 25 | 26 | //reload nginx if config has changed 27 | checksum.file(configFileName, (err, sum) => { 28 | if (sum !== checksum(newNginxConf)) { 29 | reloadNginxConfig(newNginxConf) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | 36 | export function parseServices(services) { 37 | const configs = [] 38 | //grab config from each service 39 | Object.keys(services) 40 | .filter(serviceName => services[serviceName].tags && services[serviceName].tags.nginx === NGINX_NAME) 41 | .forEach((serviceName) => { 42 | const service = services[serviceName]; 43 | 44 | Object.keys(service.hosts) 45 | .forEach((key) => { 46 | //replace monkey patched dots in host name 47 | const host = key.replace(/\*\^\*/g, ".") 48 | const { cert, ssl, upstream } = service.hosts[key] 49 | 50 | const upstreamName = snakeCase(host) 51 | //write cert file if provided 52 | if (ssl && cert) { 53 | fs.writeFileSync(`${certsPath}/${host}.crt`, trim(cert).replace(/\\n/g, "\n")) 54 | } 55 | 56 | configs.push({ 57 | host, 58 | ssl, 59 | upstream: upstream.sort(), 60 | upstreamName, 61 | }) 62 | }) 63 | }) 64 | 65 | if (configs.length && NGINX_DEBUG === "true") { 66 | console.log(configs) 67 | } else if (!configs.length) { 68 | console.log("There are no services to load balance") 69 | } 70 | 71 | return sortBy(configs, "host") 72 | } 73 | 74 | export function reloadNginxConfig(config) { 75 | fs.writeFileSync(configFileName, config) 76 | const testCmd = process.env.NGINX_RELOAD === "false" ? "" : "nginx -t" 77 | const reloadCmd = process.env.NGINX_RELOAD === "false" ? "" : "service nginx reload" 78 | console.log("Testing new Nginx config...") 79 | 80 | try { 81 | execSync(testCmd) 82 | execSync(reloadCmd) 83 | console.log('Nginx reload of /etc/nginx/conf.d/default.conf was successful') 84 | 85 | if (NGINX_DEBUG === "true") { 86 | console.log(config) 87 | } 88 | 89 | } catch(e) { 90 | configFailed(config, e) 91 | } 92 | } 93 | 94 | export function configFailed(config, stderr) { 95 | console.log("Nginx config failed", stderr) 96 | console.log(config) 97 | 98 | if (slackWebhook) { 99 | const text = `Nginx (${NGINX_NAME}) config failed: 100 | *Error:* 101 | \`\`\`${stderr}\`\`\` 102 | *Config:* 103 | \`\`\`${config}\`\`\` 104 | ` 105 | 106 | axios.post(slackWebhook, {text, username: `Nginx ${NGINX_NAME}`}) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes 1; 3 | pid /run/nginx.pid; 4 | 5 | events { 6 | worker_connections 1024; 7 | # multi_accept on; 8 | } 9 | 10 | http { 11 | 12 | ## 13 | # Basic Settings 14 | ## 15 | 16 | sendfile on; 17 | tcp_nopush on; 18 | tcp_nodelay on; 19 | keepalive_timeout 65; 20 | types_hash_max_size 2048; 21 | client_max_body_size 5m; 22 | # server_tokens off; 23 | 24 | # server_names_hash_bucket_size 64; 25 | # server_name_in_redirect off; 26 | server_names_hash_max_size 2048; 27 | include /etc/nginx/mime.types; 28 | default_type application/octet-stream; 29 | 30 | ## 31 | # Logging Settings 32 | ## 33 | 34 | access_log off; 35 | error_log /var/log/nginx/error.log; 36 | 37 | ## 38 | # Gzip Settings 39 | ## 40 | 41 | gzip on; 42 | gzip_disable "msie6"; 43 | 44 | # gzip_vary on; 45 | # gzip_proxied any; 46 | # gzip_comp_level 6; 47 | # gzip_buffers 16 8k; 48 | # gzip_http_version 1.1; 49 | gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 50 | 51 | ## 52 | # nginx-naxsi config 53 | ## 54 | # Uncomment it if you installed nginx-naxsi 55 | ## 56 | 57 | #include /etc/nginx/naxsi_core.rules; 58 | 59 | ## 60 | # nginx-passenger config 61 | ## 62 | # Uncomment it if you installed nginx-passenger 63 | ## 64 | 65 | #passenger_root /usr; 66 | #passenger_ruby /usr/bin/ruby; 67 | 68 | ## 69 | # Virtual Host Configs 70 | ## 71 | 72 | include /etc/nginx/conf.d/*.conf; 73 | } 74 | 75 | 76 | #mail { 77 | # # See sample authentication script at: 78 | # # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript 79 | # 80 | # # auth_http localhost/auth.php; 81 | # # pop3_capabilities "TOP" "USER"; 82 | # # imap_capabilities "IMAP4rev1" "UIDPLUS"; 83 | # 84 | # server { 85 | # listen localhost:110; 86 | # protocol pop3; 87 | # proxy on; 88 | # } 89 | # 90 | # server { 91 | # listen localhost:143; 92 | # protocol imap; 93 | # proxy on; 94 | # 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutum-nginx", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "tutum-watch.js", 6 | "author": "", 7 | "license": "ISC", 8 | "scripts": { 9 | "predeploy": "docker build -t willrstern/nginx-etcd:latest .", 10 | "deploy": "docker push willrstern/nginx-etcd:latest", 11 | "predeploy:alpha": "docker build -t willrstern/nginx-etcd:alpha .", 12 | "deploy:alpha": "docker push willrstern/nginx-etcd:alpha", 13 | "mocha": "mocha --compilers js:babel-register", 14 | "start": "NGINX_NAME=primary NGINX_ETCD_HOST=localhost NGINX_RELOAD=false NGINX_CERTS='./certs' NGINX_CONFIG_FILE='./default.conf' nodemon docker-cloud-watch.js", 15 | "pretest": "docker-compose up -d", 16 | "test": "docker-compose build test && docker-compose run test" 17 | }, 18 | "dependencies": { 19 | "axios": "^0.7.0", 20 | "babel-preset-es2015": "^6.3.13", 21 | "babel-register": "^6.3.13", 22 | "checksum": "^0.1.1", 23 | "etcd-flatten": "0.0.2", 24 | "hogan": "^1.0.2", 25 | "lodash": "^4.3.0", 26 | "node-etcd": "^4.2.1", 27 | "object-set": "^1.0.1", 28 | "ws": "^0.8.1" 29 | }, 30 | "devDependencies": { 31 | "mocha": "^2.4.5", 32 | "nginx-config-parser": "^0.1.1", 33 | "nodemon": "^1.9.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/etcd-watch.test.js: -------------------------------------------------------------------------------- 1 | import assert from "assert" 2 | import fs from "fs" 3 | import getServices from "../lib/etcd-watch" 4 | 5 | const apiCert = "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQC/G6eud0cQqqI729F+uxT1Zv1HLpqz/qOc/3GjtZohNUyOCJK0\ngoiiNwz9nNy62Q3iAknW/EP6TKNfiN1AFNsqECsxOku0mUAyvERLeBfWpXVXTi27\n7ml7KXoVhhDxPj3/PDsgyvrEsQ8e9/jDvkzovSKyfMJa0shbi6gwzJr+awIDAQAB\nAoGBAK0yoBZzDVnieyOqxcOIQ6dgjlzrtNM6DQglTdVjqWs9RcNXq7Wis7foEoLq\nnfVM79ML5eXMPMNkn4/elz4TaMe1tQKzeevy7waLEjLDlrtqQs4duX4ulUhQvDr2\nZnWLiaoGIN/K+QnHzR1k7Kj07sT8PL3gIwqtqRdDBSO4ljIRAkEA6wMZ/OxKJCH6\nlKQ3C+7wlXttmBhkGQpzjvC0E8yJFR2ui/SCnvL75OD5+2pDUGpoXaIeCXDAisWN\nAi/KZ9bF2QJBANAszwEUeU1cjf6vPTxrLOzcVLZSdgr3NBe8IAUtE46jL3rdfWlQ\nAYPmXfmRynLk8hZM3LgJFM1gO8JrFTJBp+MCQE3UUR76AfPFbP8dAz3oe7SFk93y\n9fN1CqAkBv8nlZ5wngWrjDansdQyzZb9sh1HoBiiP+BQfvN2SSSYPyf0cMECQQC2\nkQV92fnDyc7Rs9eNXCTLGTPFra3OUhvCUP736x9CsYRbSVHKARtDFM4HqD8W4ggZ\nXJEZaQVwU9w01fqB16inAkBW5FWZixHCqHrDHMKevB+VRGtr94s0yOAxUZPPEAT/\nwIBs4GGlzYk6sPgp8vMOHCtoox2JjzzgiZCsU2HObLS/\n-----END RSA PRIVATE KEY-----\n-----BEGIN CERTIFICATE-----\nMIIB8TCCAVoCCQCrK/E7Wz0CxTANBgkqhkiG9w0BAQUFADA9MQswCQYDVQQGEwJV\nUzELMAkGA1UECBMCVFgxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0\nZDAeFw0xNjAyMjMxNzUxMzBaFw0xNzAyMjIxNzUxMzBaMD0xCzAJBgNVBAYTAlVT\nMQswCQYDVQQIEwJUWDEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/G6eud0cQqqI729F+uxT1Zv1H\nLpqz/qOc/3GjtZohNUyOCJK0goiiNwz9nNy62Q3iAknW/EP6TKNfiN1AFNsqECsx\nOku0mUAyvERLeBfWpXVXTi277ml7KXoVhhDxPj3/PDsgyvrEsQ8e9/jDvkzovSKy\nfMJa0shbi6gwzJr+awIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAAQG+EhyuEQleHRg\nuZZnGKIYbeODAWTY4UOVNjV2AItHWk/yPDbPoxhj9e1iC7JdKHgJTLaJw0JzLuXx\nmPvyczvXfORsTv0Isc3JH71xhZ2GLX10rhQKBzIzud6CwopFmfdAAM0/z4gJ67JZ\noAPJD828GizKayML5BIu3hQSufiy\n-----END CERTIFICATE-----" 6 | 7 | describe("etcd-watch", () => { 8 | beforeEach(function (done) { 9 | getServices((services) => { 10 | this.services = services 11 | done() 12 | }) 13 | }) 14 | 15 | it("sets nginx name tags", function() { 16 | const { web, api } = this.services 17 | assert.equal(web.tags.nginx, "primary") 18 | assert.equal(api.tags.nginx, "primary") 19 | }) 20 | 21 | it("sets service ssl=true when cert is provided", function() { 22 | const { api } = this.services 23 | assert(api.hosts["api*^*com"].ssl) 24 | }) 25 | 26 | it("does not set service ssl=true when no cert is provided", function() { 27 | const { web } = this.services 28 | assert.equal(web.hosts["test*^*com"].ssl, undefined) 29 | }) 30 | 31 | it("sets ssl cert when cert is provided", function() { 32 | const { api } = this.services 33 | assert.equal(api.hosts["api*^*com"].cert, apiCert) 34 | }) 35 | 36 | it("does not set ssl cert when no cert is provided", function() { 37 | const { web } = this.services 38 | assert.equal(web.hosts["test*^*com"].cert, undefined) 39 | }) 40 | 41 | it("sets upstream:port for each host", function() { 42 | const { web } = this.services 43 | const { "test*^*com": test, "test2*^*com": test2 } = web.hosts 44 | assert(~test.upstream.indexOf("web:80")) 45 | assert(~test.upstream.indexOf("web2:80")) 46 | assert(~test2.upstream.indexOf("web:80")) 47 | assert(~test2.upstream.indexOf("web2:80")) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /test/functional.js: -------------------------------------------------------------------------------- 1 | import assert from "assert" 2 | import axios from "axios" 3 | import { execSync } from "child_process" 4 | import { readFileSync } from "fs" 5 | import { find, snakeCase } from "lodash" 6 | import nginxConfigParser from "nginx-config-parser" 7 | 8 | import getServices from "../lib/etcd-watch" 9 | import reloadNginx from "../lib/reload-nginx" 10 | import { getContainersToBalance, parseServices, generateNewConfig } from "../lib/reload-nginx" 11 | 12 | describe("nginx", function() { 13 | before(function(done) { 14 | getServices(reloadNginx) 15 | 16 | setTimeout(() => { 17 | const nginxConfigFile = readFileSync("/etc/nginx/conf.d/default.conf", "utf-8") 18 | this.config = nginxConfigParser.parseFromString( nginxConfigFile, 'utf-8') 19 | done() 20 | }, 1000) 21 | }) 22 | 23 | describe("config", function() { 24 | it("is accepted by nginx", () => { 25 | execSync("nginx -t") 26 | }) 27 | 28 | describe("upstreams", function() { 29 | it("contains running test.com upstream nodes", function() { 30 | const upstream = this.config["upstream test_com"][0].server; 31 | 32 | assert(hasUpstream(upstream, "web:80")) 33 | assert(hasUpstream(upstream, "web2:80")) 34 | assert(!hasUpstream(upstream, "api:80")) 35 | }) 36 | 37 | it("contains running test2.com upstream nodes", function() { 38 | const upstream = this.config["upstream test_2_com"][0].server; 39 | 40 | assert(hasUpstream(upstream, "web:80")) 41 | assert(hasUpstream(upstream, "web2:80")) 42 | assert(!hasUpstream(upstream, "api:80")) 43 | }) 44 | 45 | it("contains running api.com upstream node", function() { 46 | const upstream = this.config["upstream api_com"][0].server; 47 | 48 | assert(hasUpstream(upstream, "api:80")) 49 | assert(!hasUpstream(upstream, "web:80")) 50 | assert(!hasUpstream(upstream, "web1:80")) 51 | }) 52 | }) 53 | 54 | describe("servers", () => { 55 | it("proxy_pass points to correct upstream for virtual hosts", function() { 56 | const hosts = ["test.com", "test2.com", "api.com"]; 57 | 58 | hosts.forEach((host) => { 59 | assert.equal(getProxyPass(this.config.server, host), `http://${snakeCase(host)}`); 60 | }) 61 | }) 62 | }) 63 | 64 | describe("certs", () => { 65 | it("writes cert files when provided", function() { 66 | const apiCert = readFileSync("/certs/api.com.crt", "utf-8") 67 | const apiCertExpected = readFileSync(__dirname + "/mocks/api.com.crt", "utf-8") 68 | 69 | assert.equal(apiCert, apiCertExpected) 70 | }) 71 | }) 72 | }) 73 | }) 74 | 75 | 76 | function hasUpstream(upstream, url) { 77 | let exists = false; 78 | upstream.forEach((val) => { 79 | if (Array.isArray(val) && val[0] === url) { 80 | exists = true; 81 | } 82 | }) 83 | return exists; 84 | } 85 | 86 | function getProxyPass(servers, serverName) { 87 | const server = getServer(servers, serverName); 88 | return server["location /"][0].proxy_pass[0][0]; 89 | } 90 | 91 | function getServer(servers, serverName) { 92 | for (let i=0; i < servers.length; i++) { 93 | if (servers[i].server_name[0][0] === serverName) { 94 | return servers[i] 95 | } 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /test/mocks/api.com.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQC/G6eud0cQqqI729F+uxT1Zv1HLpqz/qOc/3GjtZohNUyOCJK0 3 | goiiNwz9nNy62Q3iAknW/EP6TKNfiN1AFNsqECsxOku0mUAyvERLeBfWpXVXTi27 4 | 7ml7KXoVhhDxPj3/PDsgyvrEsQ8e9/jDvkzovSKyfMJa0shbi6gwzJr+awIDAQAB 5 | AoGBAK0yoBZzDVnieyOqxcOIQ6dgjlzrtNM6DQglTdVjqWs9RcNXq7Wis7foEoLq 6 | nfVM79ML5eXMPMNkn4/elz4TaMe1tQKzeevy7waLEjLDlrtqQs4duX4ulUhQvDr2 7 | ZnWLiaoGIN/K+QnHzR1k7Kj07sT8PL3gIwqtqRdDBSO4ljIRAkEA6wMZ/OxKJCH6 8 | lKQ3C+7wlXttmBhkGQpzjvC0E8yJFR2ui/SCnvL75OD5+2pDUGpoXaIeCXDAisWN 9 | Ai/KZ9bF2QJBANAszwEUeU1cjf6vPTxrLOzcVLZSdgr3NBe8IAUtE46jL3rdfWlQ 10 | AYPmXfmRynLk8hZM3LgJFM1gO8JrFTJBp+MCQE3UUR76AfPFbP8dAz3oe7SFk93y 11 | 9fN1CqAkBv8nlZ5wngWrjDansdQyzZb9sh1HoBiiP+BQfvN2SSSYPyf0cMECQQC2 12 | kQV92fnDyc7Rs9eNXCTLGTPFra3OUhvCUP736x9CsYRbSVHKARtDFM4HqD8W4ggZ 13 | XJEZaQVwU9w01fqB16inAkBW5FWZixHCqHrDHMKevB+VRGtr94s0yOAxUZPPEAT/ 14 | wIBs4GGlzYk6sPgp8vMOHCtoox2JjzzgiZCsU2HObLS/ 15 | -----END RSA PRIVATE KEY----- 16 | -----BEGIN CERTIFICATE----- 17 | MIIB8TCCAVoCCQCrK/E7Wz0CxTANBgkqhkiG9w0BAQUFADA9MQswCQYDVQQGEwJV 18 | UzELMAkGA1UECBMCVFgxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 19 | ZDAeFw0xNjAyMjMxNzUxMzBaFw0xNzAyMjIxNzUxMzBaMD0xCzAJBgNVBAYTAlVT 20 | MQswCQYDVQQIEwJUWDEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk 21 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/G6eud0cQqqI729F+uxT1Zv1H 22 | Lpqz/qOc/3GjtZohNUyOCJK0goiiNwz9nNy62Q3iAknW/EP6TKNfiN1AFNsqECsx 23 | Oku0mUAyvERLeBfWpXVXTi277ml7KXoVhhDxPj3/PDsgyvrEsQ8e9/jDvkzovSKy 24 | fMJa0shbi6gwzJr+awIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAAQG+EhyuEQleHRg 25 | uZZnGKIYbeODAWTY4UOVNjV2AItHWk/yPDbPoxhj9e1iC7JdKHgJTLaJw0JzLuXx 26 | mPvyczvXfORsTv0Isc3JH71xhZ2GLX10rhQKBzIzud6CwopFmfdAAM0/z4gJ67JZ 27 | oAPJD828GizKayML5BIu3hQSufiy 28 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /test/mocks/test2.com.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQC8mbIRnUapfSr8Of1yMgIQTfCb6krPRk3/LNmwbW0XjkTzuFx3 3 | xjLgvADhUdW42JMIcGQBXx5wkdOMHTyj/p+JcRcDj/z+Liy76ywDUXgeQbQxZIS2 4 | O7U1Wf5qXMwLsj+vfGBndulgIPRx/4x4gt+tUNOimXJRTwSGmXFH0rXcWQIDAQAB 5 | AoGAb8ru6U//tbGTDEVXfRw1avK4H8NmKqzyyMIOG20RkDftmUX70adzOxFVuDmo 6 | 5NPDe+oa7VEzmuhlrBUcf90LNQjrvo9ei3wvji90cEvk+jQSomZnhbNqIrK7Jj8S 7 | GY+CamU19jKdDg0QXo5TATTrtZUMj+i3r93nEh81oi5Oj10CQQDrhBTrBICYqjye 8 | aU45+UH/338hKCtqGpphcWOYdMUs1wg/8Iz+5f5MKm2lI5j27LHca6CE7cTkAY56 9 | BzqvulZbAkEAzQEB9XWuT9qJYDXDV/n5KQ2+EnOMDt8vr/NKHWbiBuG+jBZdnkk3 10 | 1tepJMODurSXgaaxKrV0jvkwh+f9SBeeWwJBAI7OsUxsl3l6yHUZz4hRvxZjNBgr 11 | 3l9hMDlj4wtfyuvMm8EBoM0zMsaGd6PJ+QfJMHRCgrv33QqQcw9FcO17ZL8CQGPy 12 | qdHSnjmwgmm1zJeH+EJbwN+eFhrqFYXjR68uCeTxCsWh4eLaL68/VefmqsLMaVF8 13 | w8Pe1AUg2Nhp8sLDQSECQQDnaUWVDYi5zpSHdSjbGst3G82gYk5o73nrd0652cCN 14 | 053v5BXiHDWI2nHbTyxJPPkdCuCFD3bVKOvCPfvbeAqb 15 | -----END RSA PRIVATE KEY----- 16 | -----BEGIN CERTIFICATE----- 17 | MIIB8TCCAVoCCQCpsLAYzTOZ3jANBgkqhkiG9w0BAQUFADA9MQswCQYDVQQGEwJV 18 | UzELMAkGA1UECBMCTU8xITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 19 | ZDAeFw0xNjAyMjMxODUwNTRaFw0xNzAyMjIxODUwNTRaMD0xCzAJBgNVBAYTAlVT 20 | MQswCQYDVQQIEwJNTzEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk 21 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8mbIRnUapfSr8Of1yMgIQTfCb 22 | 6krPRk3/LNmwbW0XjkTzuFx3xjLgvADhUdW42JMIcGQBXx5wkdOMHTyj/p+JcRcD 23 | j/z+Liy76ywDUXgeQbQxZIS2O7U1Wf5qXMwLsj+vfGBndulgIPRx/4x4gt+tUNOi 24 | mXJRTwSGmXFH0rXcWQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAKbmlJ5e+mZ9Ldwb 25 | 8hol38DaEodCc07+aP8PRoJHLw5X/zQlbM0O3RbT+i1JN4ID6roCb3eqrA5uxott 26 | zmD/Xh5HhSlxpe8+PHNr9xlx8LBHKswjYBLNwYZBew9hLUEgz+RB01fSQ9TfM2SP 27 | LVldKmxmb+HMcdEQdpTqriD8XryD 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /test/mocks/test3.com.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICWwIBAAKBgQDP+QTw2JqtQQdjsxWTusqc9g2JuKok7B4VcM9O8XSrZ9Quorst 3 | cGfNBshM7khF48vJH14xjgpAOqMUg0hkP64I6QvbzisWYjk+2s1vNJWcTLWL3onO 4 | b8H7yLJ40qBmxmiVFVSx/dJr1PuS2MQxAzSHRSdpBpQzAa9QV0PhcJy9SQIDAQAB 5 | AoGADeL6yXotGdR1wdp7XlninYhwbvm9oqmBeL0HmqXUvH06VcLX7LjMtYv2Y+yl 6 | NsV3Hf7SM0zgslk3+m6prsfxvtfAMEWL+llADOLJCI3xdtF99MWPF0n/M46QRlhF 7 | 7Aab+mZq06AuPLRZTaFbFywk1B05jMD/NY9xuX7XFwgYVAECQQDoSHTz1sD5as+7 8 | EQvMUgjznZssywAWmdV3B6VAEaALBIkD6epA6Adfc07RloAdgoKrl7/NfeFnIJf6 9 | xnWpMOa5AkEA5TUgUPRl86oJ3lYq1ph5b0dPfoGudcsoyjBjr8rl0sp7UErWEfvk 10 | EuY4LBvgNfVlW3Puzp4gEuCRBACWalNDEQJActI+XbqusZxHC2Wlu15h5mrmJgJD 11 | DOkGSEyTN1R/FHMtd63NikAoRNqu/5OxyOSWy1O8EExFe8D035Xy26u8oQJAG2xE 12 | xorHG+UPMzO6AlzRwpeUkj0vw1YgNjid5K1w28xv/oZFoHczrXMv608Wfz4x90Qi 13 | oUPX8Io/r2vmkygNEQJAHWXS+kqQUwyhgip0RB4prIzU3IvIpcAlgbWfkB0ctzmV 14 | LFjO6q+jluTqIo2oYsPKHaW/X1IGFsvx2elfZa3djA== 15 | -----END RSA PRIVATE KEY----- 16 | -----BEGIN CERTIFICATE----- 17 | MIIB8TCCAVoCCQCyXTo/90kG/DANBgkqhkiG9w0BAQUFADA9MQswCQYDVQQGEwJV 18 | UzELMAkGA1UECBMCQ0ExITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 19 | ZDAeFw0xNjAyMjMxODU0MjNaFw0xNzAyMjIxODU0MjNaMD0xCzAJBgNVBAYTAlVT 20 | MQswCQYDVQQIEwJDQTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk 21 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDP+QTw2JqtQQdjsxWTusqc9g2J 22 | uKok7B4VcM9O8XSrZ9QuorstcGfNBshM7khF48vJH14xjgpAOqMUg0hkP64I6Qvb 23 | zisWYjk+2s1vNJWcTLWL3onOb8H7yLJ40qBmxmiVFVSx/dJr1PuS2MQxAzSHRSdp 24 | BpQzAa9QV0PhcJy9SQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAEgqLO5vCoRc/WAj 25 | pz63kYragZBWA3UzoPyfJhomHf6S12GJqmYTI1J0afbLkEJEypMZjuyCjrmaoNWK 26 | vgMbPZ+wku9PW1RPNhMk480XcuF9MjM4QKhQxO7PmyV5cfollU6L0pnlxc6BYyr3 27 | Qvoq/i05acyJtgRpU6U7NrtWoYi5 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /test/mocks/test5.com.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQC/G6eud0cQqqI729F+uxT1Zv1HLpqz/qOc/3GjtZohNUyOCJK0 3 | goiiNwz9nNy62Q3iAknW/EP6TKNfiN1AFNsqECsxOku0mUAyvERLeBfWpXVXTi27 4 | 7ml7KXoVhhDxPj3/PDsgyvrEsQ8e9/jDvkzovSKyfMJa0shbi6gwzJr+awIDAQAB 5 | AoGBAK0yoBZzDVnieyOqxcOIQ6dgjlzrtNM6DQglTdVjqWs9RcNXq7Wis7foEoLq 6 | nfVM79ML5eXMPMNkn4/elz4TaMe1tQKzeevy7waLEjLDlrtqQs4duX4ulUhQvDr2 7 | ZnWLiaoGIN/K+QnHzR1k7Kj07sT8PL3gIwqtqRdDBSO4ljIRAkEA6wMZ/OxKJCH6 8 | lKQ3C+7wlXttmBhkGQpzjvC0E8yJFR2ui/SCnvL75OD5+2pDUGpoXaIeCXDAisWN 9 | Ai/KZ9bF2QJBANAszwEUeU1cjf6vPTxrLOzcVLZSdgr3NBe8IAUtE46jL3rdfWlQ 10 | AYPmXfmRynLk8hZM3LgJFM1gO8JrFTJBp+MCQE3UUR76AfPFbP8dAz3oe7SFk93y 11 | 9fN1CqAkBv8nlZ5wngWrjDansdQyzZb9sh1HoBiiP+BQfvN2SSSYPyf0cMECQQC2 12 | kQV92fnDyc7Rs9eNXCTLGTPFra3OUhvCUP736x9CsYRbSVHKARtDFM4HqD8W4ggZ 13 | XJEZaQVwU9w01fqB16inAkBW5FWZixHCqHrDHMKevB+VRGtr94s0yOAxUZPPEAT/ 14 | wIBs4GGlzYk6sPgp8vMOHCtoox2JjzzgiZCsU2HObLS/ 15 | -----END RSA PRIVATE KEY----- 16 | -----BEGIN CERTIFICATE----- 17 | MIIB8TCCAVoCCQCrK/E7Wz0CxTANBgkqhkiG9w0BAQUFADA9MQswCQYDVQQGEwJV 18 | UzELMAkGA1UECBMCVFgxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 19 | ZDAeFw0xNjAyMjMxNzUxMzBaFw0xNzAyMjIxNzUxMzBaMD0xCzAJBgNVBAYTAlVT 20 | MQswCQYDVQQIEwJUWDEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk 21 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/G6eud0cQqqI729F+uxT1Zv1H 22 | Lpqz/qOc/3GjtZohNUyOCJK0goiiNwz9nNy62Q3iAknW/EP6TKNfiN1AFNsqECsx 23 | Oku0mUAyvERLeBfWpXVXTi277ml7KXoVhhDxPj3/PDsgyvrEsQ8e9/jDvkzovSKy 24 | fMJa0shbi6gwzJr+awIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAAQG+EhyuEQleHRg 25 | uZZnGKIYbeODAWTY4UOVNjV2AItHWk/yPDbPoxhj9e1iC7JdKHgJTLaJw0JzLuXx 26 | mPvyczvXfORsTv0Isc3JH71xhZ2GLX10rhQKBzIzud6CwopFmfdAAM0/z4gJ67JZ 27 | oAPJD828GizKayML5BIu3hQSufiy 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /test/mocks/test6.com.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQDBGfBFeEBBW3N6q51tAYFLdiMZRwwjsuv/5VeDLJ1TYAQ1F999 3 | NqVI7LONhr5/7vC8hn4P3vcaQAx3WQ8mbU1nov3d35TvVJp4csVxfF2k0w5OvDx/ 4 | Zy6vPFgjTkGMyaM1HDdBbIfPweRIotYOoSnOj0MH9wQeRLeE7LRWIbMHCwIDAQAB 5 | AoGAPL50HuZdEDI8eXJS61911M8s6162KuS16KG0jccTFo81w53m5/Swuef786FX 6 | e9cmU6fbMBLrmI5dXY3efjAUEOE1cqeYNcjts38CmxRrBHksRoN+b6hS7Eza93hU 7 | iPmLNWs0U1ZL+gjXVESn1SYi27Dv+fIS7ypyJqk8l8zaEoECQQD3+DRtIyMEFVsf 8 | pvCzzecCGiy+11pHlGmaone9UK2ZQKWlfxFdCwzrLeKNLZX0ah+JnbLMiu/cAeEX 9 | s8DvuY9TAkEAx1rZEVp8VNkAjoxY9cjzTwHYexk1uoqePvl3RbDH95wXYYnQXPs3 10 | mmVfeol0barmG9o+yfQMw+M5MqPJO0oKaQJBAIA1j/3BxhANbrD94sREuKVInUwL 11 | Er2hybxPNnPm6+sqFrtr6LFfzk0my1VNdZQK7sV7iP/i8kOhoR1dLmuEWMsCQEs0 12 | P1zniK2tayNbWJfn5blxVcwiV312m3ngPBljNhx3mu5lwd/BuVkaUulz/yL77HCn 13 | ZTZkRYiEKGitFtWx+bkCQQCIuoCenvucgwFrjY6BS5lxqz5rbloXVc+zJCqQMQ0/ 14 | VMpRh/Y2yMt+j8XpPoTuK/DuoaiKlteltL1VkYqgNesQ 15 | -----END RSA PRIVATE KEY----- 16 | -----BEGIN CERTIFICATE----- 17 | MIIB8TCCAVoCCQC82HieA5SU1DANBgkqhkiG9w0BAQUFADA9MQswCQYDVQQGEwJV 18 | UzELMAkGA1UECBMCTlkxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 19 | ZDAeFw0xNjAyMjMxODU1MzhaFw0xNzAyMjIxODU1MzhaMD0xCzAJBgNVBAYTAlVT 20 | MQswCQYDVQQIEwJOWTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk 21 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBGfBFeEBBW3N6q51tAYFLdiMZ 22 | Rwwjsuv/5VeDLJ1TYAQ1F999NqVI7LONhr5/7vC8hn4P3vcaQAx3WQ8mbU1nov3d 23 | 35TvVJp4csVxfF2k0w5OvDx/Zy6vPFgjTkGMyaM1HDdBbIfPweRIotYOoSnOj0MH 24 | 9wQeRLeE7LRWIbMHCwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFr836bfN8oH5B/h 25 | JGdKTNtEEARLIpIKZVBP3l27pWAEq4B7m+2Bla1UWSUuLqU447qgFfoBSjyFiXJw 26 | wNHggOOXJeb5HFb7iFB9yD3tPzV+B8hNtYSf2uGCgogyApiB0KcVUwXX1i2Pyc16 27 | XJIi2G0/Px6UggF4WPqqSTvGLT1x 28 | -----END CERTIFICATE----- 29 | --------------------------------------------------------------------------------