├── .gitignore ├── README.md ├── docker ├── README.md ├── docker-compose_npm.yml └── internal │ └── certificate.js ├── library ├── README.md └── npm_proxy.py ├── pl_npm-management.yml └── roles └── npm-management ├── .travis.yml ├── README.md ├── defaults └── main.yml ├── handlers └── main.yml ├── meta └── main.yml ├── tasks └── main.yml └── vars ├── api_secret.yml └── main.yml /.gitignore: -------------------------------------------------------------------------------- 1 | ## Created by .ignore support plugin (hsz.mobi) 2 | # Ansible 3 | hosts* 4 | !hosts.save 5 | logfile 6 | vmname.yml 7 | pl_vars/role_variable.* 8 | pl_vars/role_new-vm_win.* 9 | *.log 10 | pl_test* 11 | test* 12 | 13 | # User-Probe 14 | Probe/ **probe.py 15 | .vscode/ 16 | 17 | # User-specific stuff: 18 | .idea/ 19 | ### Python template 20 | # Byte-compiled / optimized / DLL files 21 | __pycache__/ *.py[cod] *$py.class 22 | # C extensions 23 | *.so 24 | # Distribution / packaging 25 | .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ 26 | .installed.cfg *.egg MANIFEST 27 | # PyInstaller Usually these files are written by a python script from a template before PyInstaller builds the 28 | # exe, so as to inject date/other infos into it. 29 | *.manifest *.spec 30 | # Installer logs 31 | pip-log.txt pip-delete-this-directory.txt 32 | # Unit test / coverage reports 33 | htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ 34 | # Translations 35 | *.mo *.pot 36 | # Django stuff: 37 | *.log .static_storage/ .media/ local_settings.py 38 | # Flask stuff: 39 | instance/ .webassets-cache 40 | # Scrapy stuff: 41 | .scrapy 42 | # Sphinx documentation 43 | docs/_build/ 44 | # PyBuilder 45 | target/ 46 | # Jupyter Notebook 47 | .ipynb_checkpoints 48 | # pyenv 49 | .python-version 50 | # celery beat schedule file 51 | celerybeat-schedule 52 | # SageMath parsed files 53 | *.sage.py 54 | # Environments 55 | .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ 56 | # Spyder project settings 57 | .spyderproject .spyproject 58 | # Rope project settings 59 | .ropeproject 60 | # mkdocs documentation 61 | /site 62 | # mypy 63 | .mypy_cache/ 64 | ### Linux template 65 | *~ 66 | # temporary files which can be created if a process still has a handle open of a deleted file 67 | .fuse_hidden* 68 | # KDE directory preferences 69 | .directory 70 | # Linux trash folder which might appear on any partition or disk 71 | .Trash-* 72 | # .nfs files are created when an open file is removed but is still being accessed 73 | .nfs* 74 | ### macOS template 75 | # General 76 | .DS_Store .AppleDouble .LSOverride 77 | # Icon must end with two \r 78 | Icon 79 | # Thumbnails 80 | ._* 81 | # Files that might appear in the root of a volume 82 | .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns 83 | .com.apple.timemachine.donotpresent 84 | # Directories potentially created on remote AFP share 85 | .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk 86 | ### Windows template 87 | # Windows thumbnail cache files 88 | Thumbs.db ehthumbs.db ehthumbs_vista.db 89 | # Dump file 90 | *.stackdump 91 | # Folder config file 92 | [Dd]esktop.ini 93 | # Recycle Bin used on file shares 94 | $RECYCLE.BIN/ 95 | # Windows Installer files 96 | *.cab *.msi *.msix *.msm *.msp 97 | # Windows shortcuts 98 | *.lnk 99 | ### VirtualEnv template 100 | [Bb]in [Ii]nclude [Ll]ib [Ll]ib64 [Ll]ocal [Ss]cripts pyvenv.cfg pip-selfcheck.json 101 | # CMake 102 | cmake-build-debug/ cmake-build-release/ 103 | # File-based project format 104 | *.iws 105 | # IntelliJ 106 | out/ 107 | # mpeltonen/sbt-idea plugin 108 | .idea_modules/ 109 | # JIRA plugin 110 | atlassian-ide-plugin.xml 111 | # Crashlytics plugin (for Android Studio and IntelliJ) 112 | com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties 113 | # Editor-based Rest Client 114 | .idea/httpRequests 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Ansible role for [Nginx Proxy Manager v2.10.3](https://github.com/NginxProxyManager/nginx-proxy-manager/tree/v2.10.3). 2 | a simple way to add a new proxy host via ansible playbook. 3 | Checked for version v2.10.3. 4 | 5 | Description 6 | ----------- 7 | module: nginx-proxy-manager-ansible 8 | description: a simple way to add a new proxy host or to delete via ansible playbook 9 | 10 | Requirements 11 | ------------ 12 | 13 | This role requires Ansible 2.7 or higher, Docker and Docker-Compose. 14 | 15 | Change and update a [docker-compose.yml](https://github.com/DenAV/nginx-proxy-manager-ansible/blob/main/docker/docker-compose_npm.yml) file. Bring up your stack by running docker-compose, further info [here](https://github.com/DenAV/nginx-proxy-manager-ansible/tree/main/docker). 16 | 17 | Role Variables 18 | -------------- 19 | 20 | - `npm_api_url` - IP for the Nginx Proxy Manager REST API. Default to `http://192.168.1.5:81/api`. 21 | - `npm_user` - User to authenticate the Nginx Proxy Manager REST API. 22 | - `npm_password` - Password to authenticate the Nginx Proxy Manager REST API. 23 | - `npm_access_token` - Tokens are required to authenticate against the API. 24 | 25 | - `npm_api_domain_name` - Domain Names are required to create the Proxy host. 26 | - `npm_api_host` - Forward Hostname / IP are required to create the Proxy host. 27 | - `npm_api_ssl_forced` - Is SSL Forced? Default is `False`. 28 | - `npm_api_create_host` - IWhether to create (present), or no a proxy host. Default is `False`. 29 | 30 | See the [`defaults/main.yml`](https://github.com/DenAV/nginx-proxy-manager-ansible/blob/main/roles/npm-management/defaults/main.yml) or [`vars/*.yml`](https://github.com/DenAV/nginx-proxy-manager-ansible/tree/main/roles/npm-management/vars) file listing all possible options which you can be passed to a runner registration command. 31 | 32 | Example Playbook 33 | ---------------- 34 | 35 | ```yaml 36 | - name: NPM - create proxy host 37 | hosts: localhost 38 | gather_facts: no 39 | 40 | roles: 41 | - role: npm-management 42 | npm_api_domain_name: "site-2.example.com" 43 | npm_api_host: "172.16.1.2" 44 | npm_api_ssl_forced: True 45 | npm_api_create_host: True 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | ## Quick Setup 2 | 3 | 1. Install Docker and Docker-Compose 4 | 5 | - [Docker Install documentation](https://docs.docker.com/install/) 6 | - [Docker-Compose Install documentation](https://docs.docker.com/compose/install/) 7 | 8 | 2. change and update to register with the specified e-mail address in docker-compose_npm.yml file similar to this: 9 | 10 | ```yml 11 | version: '3.8' 12 | services: 13 | app: 14 | image: 'jc21/nginx-proxy-manager:2.10.3' 15 | restart: unless-stopped 16 | ports: 17 | - '80:80' 18 | - '81:81' 19 | - '443:443' 20 | volumes: 21 | - ./data:/data 22 | - ./letsencrypt:/etc/letsencrypt 23 | - ${PWD}/internal/certificate.js:/app/internal/certificate.js 24 | environment: 25 | LE_MAIL: "npm-admin@example.com" 26 | ``` 27 | 28 | 3. Bring up your stack by running 29 | 30 | ```bash 31 | docker-compose -f docker-compose_npm.yml up -d 32 | 33 | ``` 34 | 35 | 4. Log in to the Admin UI 36 | 37 | When your docker container is running, connect to it on port `81` for the admin interface. 38 | Sometimes this can take a little bit because of the entropy of keys. 39 | 40 | [http://127.0.0.1:81](http://127.0.0.1:81) 41 | 42 | Default Admin User: 43 | ``` 44 | Email: admin@example.com 45 | Password: changeme 46 | ``` 47 | 48 | Immediately after logging in with this default user you will be asked to modify your details and change your password. 49 | -------------------------------------------------------------------------------- /docker/docker-compose_npm.yml: -------------------------------------------------------------------------------- 1 | #Version 2.10.3 2 | # LE_MAIL variable for letsencrypt_email 3 | 4 | version: '3.8' 5 | services: 6 | app: 7 | image: 'jc21/nginx-proxy-manager:2.10.3' 8 | container_name: npm-app 9 | hostname: npm 10 | ports: 11 | - '80:80' 12 | - '81:81' 13 | - '443:443' 14 | volumes: 15 | - ./data:/data 16 | - ./letsencrypt:/etc/letsencrypt 17 | - ${PWD}/internal/certificate.js:/app/internal/certificate.js 18 | restart: unless-stopped 19 | healthcheck: 20 | test: ["CMD", "/bin/check-health"] 21 | interval: 10s 22 | timeout: 3s 23 | environment: 24 | LE_MAIL: "npm-admin@example.com" 25 | -------------------------------------------------------------------------------- /docker/internal/certificate.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const https = require('https'); 4 | const tempWrite = require('temp-write'); 5 | const moment = require('moment'); 6 | const logger = require('../logger').ssl; 7 | const config = require('../lib/config'); 8 | const error = require('../lib/error'); 9 | const utils = require('../lib/utils'); 10 | const certificateModel = require('../models/certificate'); 11 | const dnsPlugins = require('../global/certbot-dns-plugins'); 12 | const internalAuditLog = require('./audit-log'); 13 | const internalNginx = require('./nginx'); 14 | const internalHost = require('./host'); 15 | const archiver = require('archiver'); 16 | const path = require('path'); 17 | const { isArray } = require('lodash'); 18 | 19 | const letsencryptStaging = config.useLetsencryptStaging(); 20 | const letsencryptConfig = '/etc/letsencrypt.ini'; 21 | const certbotCommand = 'certbot'; 22 | 23 | const le_mail = process.env.LE_MAIL; 24 | 25 | function omissions() { 26 | return ['is_deleted']; 27 | } 28 | 29 | const internalCertificate = { 30 | 31 | allowedSslFiles: ['certificate', 'certificate_key', 'intermediate_certificate'], 32 | intervalTimeout: 1000 * 60 * 60, // 1 hour 33 | interval: null, 34 | intervalProcessing: false, 35 | 36 | initTimer: () => { 37 | logger.info('Let\'s Encrypt Renewal Timer initialized'); 38 | internalCertificate.interval = setInterval(internalCertificate.processExpiringHosts, internalCertificate.intervalTimeout); 39 | // And do this now as well 40 | internalCertificate.processExpiringHosts(); 41 | }, 42 | 43 | /** 44 | * Triggered by a timer, this will check for expiring hosts and renew their ssl certs if required 45 | */ 46 | processExpiringHosts: () => { 47 | if (!internalCertificate.intervalProcessing) { 48 | internalCertificate.intervalProcessing = true; 49 | logger.info('Renewing SSL certs close to expiry...'); 50 | 51 | const cmd = certbotCommand + ' renew --non-interactive --quiet ' + 52 | '--config "' + letsencryptConfig + '" ' + 53 | '--work-dir "/tmp/letsencrypt-lib" ' + 54 | '--logs-dir "/tmp/letsencrypt-log" ' + 55 | '--preferred-challenges "dns,http" ' + 56 | '--disable-hook-validation ' + 57 | (letsencryptStaging ? '--staging' : ''); 58 | 59 | return utils.exec(cmd) 60 | .then((result) => { 61 | if (result) { 62 | logger.info('Renew Result: ' + result); 63 | } 64 | 65 | return internalNginx.reload() 66 | .then(() => { 67 | logger.info('Renew Complete'); 68 | return result; 69 | }); 70 | }) 71 | .then(() => { 72 | // Now go and fetch all the letsencrypt certs from the db and query the files and update expiry times 73 | return certificateModel 74 | .query() 75 | .where('is_deleted', 0) 76 | .andWhere('provider', 'letsencrypt') 77 | .then((certificates) => { 78 | if (certificates && certificates.length) { 79 | let promises = []; 80 | 81 | certificates.map(function (certificate) { 82 | promises.push( 83 | internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem') 84 | .then((cert_info) => { 85 | return certificateModel 86 | .query() 87 | .where('id', certificate.id) 88 | .andWhere('provider', 'letsencrypt') 89 | .patch({ 90 | expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') 91 | }); 92 | }) 93 | .catch((err) => { 94 | // Don't want to stop the train here, just log the error 95 | logger.error(err.message); 96 | }) 97 | ); 98 | }); 99 | 100 | return Promise.all(promises); 101 | } 102 | }); 103 | }) 104 | .then(() => { 105 | internalCertificate.intervalProcessing = false; 106 | }) 107 | .catch((err) => { 108 | logger.error(err); 109 | internalCertificate.intervalProcessing = false; 110 | }); 111 | } 112 | }, 113 | 114 | /** 115 | * @param {Access} access 116 | * @param {Object} data 117 | * @returns {Promise} 118 | */ 119 | create: (access, data) => { 120 | return access.can('certificates:create', data) 121 | .then(() => { 122 | data.owner_user_id = access.token.getUserId(1); 123 | 124 | if (data.provider === 'letsencrypt') { 125 | data.nice_name = data.domain_names.join(', '); 126 | } 127 | 128 | return certificateModel 129 | .query() 130 | .insertAndFetch(data) 131 | .then(utils.omitRow(omissions())); 132 | }) 133 | .then((certificate) => { 134 | if (certificate.provider === 'letsencrypt') { 135 | // Request a new Cert from LE. Let the fun begin. 136 | 137 | // 1. Find out any hosts that are using any of the hostnames in this cert 138 | // 2. Disable them in nginx temporarily 139 | // 3. Generate the LE config 140 | // 4. Request cert 141 | // 5. Remove LE config 142 | // 6. Re-instate previously disabled hosts 143 | 144 | // 1. Find out any hosts that are using any of the hostnames in this cert 145 | return internalHost.getHostsWithDomains(certificate.domain_names) 146 | .then((in_use_result) => { 147 | // 2. Disable them in nginx temporarily 148 | return internalCertificate.disableInUseHosts(in_use_result) 149 | .then(() => { 150 | return in_use_result; 151 | }); 152 | }) 153 | .then((in_use_result) => { 154 | // With DNS challenge no config is needed, so skip 3 and 5. 155 | if (certificate.meta.dns_challenge) { 156 | return internalNginx.reload().then(() => { 157 | // 4. Request cert 158 | return internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate); 159 | }) 160 | .then(internalNginx.reload) 161 | .then(() => { 162 | // 6. Re-instate previously disabled hosts 163 | return internalCertificate.enableInUseHosts(in_use_result); 164 | }) 165 | .then(() => { 166 | return certificate; 167 | }) 168 | .catch((err) => { 169 | // In the event of failure, revert things and throw err back 170 | return internalCertificate.enableInUseHosts(in_use_result) 171 | .then(internalNginx.reload) 172 | .then(() => { 173 | throw err; 174 | }); 175 | }); 176 | } else { 177 | // 3. Generate the LE config 178 | return internalNginx.generateLetsEncryptRequestConfig(certificate) 179 | .then(internalNginx.reload) 180 | .then(async() => await new Promise((r) => setTimeout(r, 5000))) 181 | .then(() => { 182 | // 4. Request cert 183 | return internalCertificate.requestLetsEncryptSsl(certificate); 184 | }) 185 | .then(() => { 186 | // 5. Remove LE config 187 | return internalNginx.deleteLetsEncryptRequestConfig(certificate); 188 | }) 189 | .then(internalNginx.reload) 190 | .then(() => { 191 | // 6. Re-instate previously disabled hosts 192 | return internalCertificate.enableInUseHosts(in_use_result); 193 | }) 194 | .then(() => { 195 | return certificate; 196 | }) 197 | .catch((err) => { 198 | // In the event of failure, revert things and throw err back 199 | return internalNginx.deleteLetsEncryptRequestConfig(certificate) 200 | .then(() => { 201 | return internalCertificate.enableInUseHosts(in_use_result); 202 | }) 203 | .then(internalNginx.reload) 204 | .then(() => { 205 | throw err; 206 | }); 207 | }); 208 | } 209 | }) 210 | .then(() => { 211 | // At this point, the letsencrypt cert should exist on disk. 212 | // Lets get the expiry date from the file and update the row silently 213 | return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem') 214 | .then((cert_info) => { 215 | return certificateModel 216 | .query() 217 | .patchAndFetchById(certificate.id, { 218 | expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') 219 | }) 220 | .then((saved_row) => { 221 | // Add cert data for audit log 222 | saved_row.meta = _.assign({}, saved_row.meta, { 223 | letsencrypt_certificate: cert_info 224 | }); 225 | 226 | return saved_row; 227 | }); 228 | }); 229 | }).catch(async (error) => { 230 | // Delete the certificate from the database if it was not created successfully 231 | await certificateModel 232 | .query() 233 | .deleteById(certificate.id); 234 | 235 | throw error; 236 | }); 237 | } else { 238 | return certificate; 239 | } 240 | }).then((certificate) => { 241 | 242 | data.meta = _.assign({}, data.meta || {}, certificate.meta); 243 | 244 | // Add to audit log 245 | return internalAuditLog.add(access, { 246 | action: 'created', 247 | object_type: 'certificate', 248 | object_id: certificate.id, 249 | meta: data 250 | }) 251 | .then(() => { 252 | return certificate; 253 | }); 254 | }); 255 | }, 256 | 257 | /** 258 | * @param {Access} access 259 | * @param {Object} data 260 | * @param {Number} data.id 261 | * @param {String} [data.email] 262 | * @param {String} [data.name] 263 | * @return {Promise} 264 | */ 265 | update: (access, data) => { 266 | return access.can('certificates:update', data.id) 267 | .then((/*access_data*/) => { 268 | return internalCertificate.get(access, {id: data.id}); 269 | }) 270 | .then((row) => { 271 | if (row.id !== data.id) { 272 | // Sanity check that something crazy hasn't happened 273 | throw new error.InternalValidationError('Certificate could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); 274 | } 275 | 276 | return certificateModel 277 | .query() 278 | .patchAndFetchById(row.id, data) 279 | .then(utils.omitRow(omissions())) 280 | .then((saved_row) => { 281 | saved_row.meta = internalCertificate.cleanMeta(saved_row.meta); 282 | data.meta = internalCertificate.cleanMeta(data.meta); 283 | 284 | // Add row.nice_name for custom certs 285 | if (saved_row.provider === 'other') { 286 | data.nice_name = saved_row.nice_name; 287 | } 288 | 289 | // Add to audit log 290 | return internalAuditLog.add(access, { 291 | action: 'updated', 292 | object_type: 'certificate', 293 | object_id: row.id, 294 | meta: _.omit(data, ['expires_on']) // this prevents json circular reference because expires_on might be raw 295 | }) 296 | .then(() => { 297 | return saved_row; 298 | }); 299 | }); 300 | }); 301 | }, 302 | 303 | /** 304 | * @param {Access} access 305 | * @param {Object} data 306 | * @param {Number} data.id 307 | * @param {Array} [data.expand] 308 | * @param {Array} [data.omit] 309 | * @return {Promise} 310 | */ 311 | get: (access, data) => { 312 | if (typeof data === 'undefined') { 313 | data = {}; 314 | } 315 | 316 | return access.can('certificates:get', data.id) 317 | .then((access_data) => { 318 | let query = certificateModel 319 | .query() 320 | .where('is_deleted', 0) 321 | .andWhere('id', data.id) 322 | .allowGraph('[owner]') 323 | .first(); 324 | 325 | if (access_data.permission_visibility !== 'all') { 326 | query.andWhere('owner_user_id', access.token.getUserId(1)); 327 | } 328 | 329 | if (typeof data.expand !== 'undefined' && data.expand !== null) { 330 | query.withGraphFetched('[' + data.expand.join(', ') + ']'); 331 | } 332 | 333 | return query.then(utils.omitRow(omissions())); 334 | }) 335 | .then((row) => { 336 | if (!row) { 337 | throw new error.ItemNotFoundError(data.id); 338 | } 339 | // Custom omissions 340 | if (typeof data.omit !== 'undefined' && data.omit !== null) { 341 | row = _.omit(row, data.omit); 342 | } 343 | return row; 344 | }); 345 | }, 346 | 347 | /** 348 | * @param {Access} access 349 | * @param {Object} data 350 | * @param {Number} data.id 351 | * @returns {Promise} 352 | */ 353 | download: (access, data) => { 354 | return new Promise((resolve, reject) => { 355 | access.can('certificates:get', data) 356 | .then(() => { 357 | return internalCertificate.get(access, data); 358 | }) 359 | .then((certificate) => { 360 | if (certificate.provider === 'letsencrypt') { 361 | const zipDirectory = '/etc/letsencrypt/live/npm-' + data.id; 362 | 363 | if (!fs.existsSync(zipDirectory)) { 364 | throw new error.ItemNotFoundError('Certificate ' + certificate.nice_name + ' does not exists'); 365 | } 366 | 367 | let certFiles = fs.readdirSync(zipDirectory) 368 | .filter((fn) => fn.endsWith('.pem')) 369 | .map((fn) => fs.realpathSync(path.join(zipDirectory, fn))); 370 | const downloadName = 'npm-' + data.id + '-' + `${Date.now()}.zip`; 371 | const opName = '/tmp/' + downloadName; 372 | internalCertificate.zipFiles(certFiles, opName) 373 | .then(() => { 374 | logger.debug('zip completed : ', opName); 375 | const resp = { 376 | fileName: opName 377 | }; 378 | resolve(resp); 379 | }).catch((err) => reject(err)); 380 | } else { 381 | throw new error.ValidationError('Only Let\'sEncrypt certificates can be downloaded'); 382 | } 383 | }).catch((err) => reject(err)); 384 | }); 385 | }, 386 | 387 | /** 388 | * @param {String} source 389 | * @param {String} out 390 | * @returns {Promise} 391 | */ 392 | zipFiles(source, out) { 393 | const archive = archiver('zip', { zlib: { level: 9 } }); 394 | const stream = fs.createWriteStream(out); 395 | 396 | return new Promise((resolve, reject) => { 397 | source 398 | .map((fl) => { 399 | let fileName = path.basename(fl); 400 | logger.debug(fl, 'added to certificate zip'); 401 | archive.file(fl, { name: fileName }); 402 | }); 403 | archive 404 | .on('error', (err) => reject(err)) 405 | .pipe(stream); 406 | 407 | stream.on('close', () => resolve()); 408 | archive.finalize(); 409 | }); 410 | }, 411 | 412 | /** 413 | * @param {Access} access 414 | * @param {Object} data 415 | * @param {Number} data.id 416 | * @param {String} [data.reason] 417 | * @returns {Promise} 418 | */ 419 | delete: (access, data) => { 420 | return access.can('certificates:delete', data.id) 421 | .then(() => { 422 | return internalCertificate.get(access, {id: data.id}); 423 | }) 424 | .then((row) => { 425 | if (!row) { 426 | throw new error.ItemNotFoundError(data.id); 427 | } 428 | 429 | return certificateModel 430 | .query() 431 | .where('id', row.id) 432 | .patch({ 433 | is_deleted: 1 434 | }) 435 | .then(() => { 436 | // Add to audit log 437 | row.meta = internalCertificate.cleanMeta(row.meta); 438 | 439 | return internalAuditLog.add(access, { 440 | action: 'deleted', 441 | object_type: 'certificate', 442 | object_id: row.id, 443 | meta: _.omit(row, omissions()) 444 | }); 445 | }) 446 | .then(() => { 447 | if (row.provider === 'letsencrypt') { 448 | // Revoke the cert 449 | return internalCertificate.revokeLetsEncryptSsl(row); 450 | } 451 | }); 452 | }) 453 | .then(() => { 454 | return true; 455 | }); 456 | }, 457 | 458 | /** 459 | * All Certs 460 | * 461 | * @param {Access} access 462 | * @param {Array} [expand] 463 | * @param {String} [search_query] 464 | * @returns {Promise} 465 | */ 466 | getAll: (access, expand, search_query) => { 467 | return access.can('certificates:list') 468 | .then((access_data) => { 469 | let query = certificateModel 470 | .query() 471 | .where('is_deleted', 0) 472 | .groupBy('id') 473 | .allowGraph('[owner]') 474 | .orderBy('nice_name', 'ASC'); 475 | 476 | if (access_data.permission_visibility !== 'all') { 477 | query.andWhere('owner_user_id', access.token.getUserId(1)); 478 | } 479 | 480 | // Query is used for searching 481 | if (typeof search_query === 'string') { 482 | query.where(function () { 483 | this.where('nice_name', 'like', '%' + search_query + '%'); 484 | }); 485 | } 486 | 487 | if (typeof expand !== 'undefined' && expand !== null) { 488 | query.withGraphFetched('[' + expand.join(', ') + ']'); 489 | } 490 | 491 | return query.then(utils.omitRows(omissions())); 492 | }); 493 | }, 494 | 495 | /** 496 | * Report use 497 | * 498 | * @param {Number} user_id 499 | * @param {String} visibility 500 | * @returns {Promise} 501 | */ 502 | getCount: (user_id, visibility) => { 503 | let query = certificateModel 504 | .query() 505 | .count('id as count') 506 | .where('is_deleted', 0); 507 | 508 | if (visibility !== 'all') { 509 | query.andWhere('owner_user_id', user_id); 510 | } 511 | 512 | return query.first() 513 | .then((row) => { 514 | return parseInt(row.count, 10); 515 | }); 516 | }, 517 | 518 | /** 519 | * @param {Object} certificate 520 | * @returns {Promise} 521 | */ 522 | writeCustomCert: (certificate) => { 523 | logger.info('Writing Custom Certificate:', certificate); 524 | 525 | const dir = '/data/custom_ssl/npm-' + certificate.id; 526 | 527 | return new Promise((resolve, reject) => { 528 | if (certificate.provider === 'letsencrypt') { 529 | reject(new Error('Refusing to write letsencrypt certs here')); 530 | return; 531 | } 532 | 533 | let certData = certificate.meta.certificate; 534 | if (typeof certificate.meta.intermediate_certificate !== 'undefined') { 535 | certData = certData + '\n' + certificate.meta.intermediate_certificate; 536 | } 537 | 538 | try { 539 | if (!fs.existsSync(dir)) { 540 | fs.mkdirSync(dir); 541 | } 542 | } catch (err) { 543 | reject(err); 544 | return; 545 | } 546 | 547 | fs.writeFile(dir + '/fullchain.pem', certData, function (err) { 548 | if (err) { 549 | reject(err); 550 | } else { 551 | resolve(); 552 | } 553 | }); 554 | }) 555 | .then(() => { 556 | return new Promise((resolve, reject) => { 557 | fs.writeFile(dir + '/privkey.pem', certificate.meta.certificate_key, function (err) { 558 | if (err) { 559 | reject(err); 560 | } else { 561 | resolve(); 562 | } 563 | }); 564 | }); 565 | }); 566 | }, 567 | 568 | /** 569 | * @param {Access} access 570 | * @param {Object} data 571 | * @param {Array} data.domain_names 572 | * @param {String} data.meta.letsencrypt_email 573 | * @param {Boolean} data.meta.letsencrypt_agree 574 | * @returns {Promise} 575 | */ 576 | createQuickCertificate: (access, data) => { 577 | return internalCertificate.create(access, { 578 | provider: 'letsencrypt', 579 | domain_names: data.domain_names, 580 | meta: data.meta 581 | }); 582 | }, 583 | 584 | /** 585 | * Validates that the certs provided are good. 586 | * No access required here, nothing is changed or stored. 587 | * 588 | * @param {Object} data 589 | * @param {Object} data.files 590 | * @returns {Promise} 591 | */ 592 | validate: (data) => { 593 | return new Promise((resolve) => { 594 | // Put file contents into an object 595 | let files = {}; 596 | _.map(data.files, (file, name) => { 597 | if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) { 598 | files[name] = file.data.toString(); 599 | } 600 | }); 601 | 602 | resolve(files); 603 | }) 604 | .then((files) => { 605 | // For each file, create a temp file and write the contents to it 606 | // Then test it depending on the file type 607 | let promises = []; 608 | _.map(files, (content, type) => { 609 | promises.push(new Promise((resolve) => { 610 | if (type === 'certificate_key') { 611 | resolve(internalCertificate.checkPrivateKey(content)); 612 | } else { 613 | // this should handle `certificate` and intermediate certificate 614 | resolve(internalCertificate.getCertificateInfo(content, true)); 615 | } 616 | }).then((res) => { 617 | return {[type]: res}; 618 | })); 619 | }); 620 | 621 | return Promise.all(promises) 622 | .then((files) => { 623 | let data = {}; 624 | 625 | _.each(files, (file) => { 626 | data = _.assign({}, data, file); 627 | }); 628 | 629 | return data; 630 | }); 631 | }); 632 | }, 633 | 634 | /** 635 | * @param {Access} access 636 | * @param {Object} data 637 | * @param {Number} data.id 638 | * @param {Object} data.files 639 | * @returns {Promise} 640 | */ 641 | upload: (access, data) => { 642 | return internalCertificate.get(access, {id: data.id}) 643 | .then((row) => { 644 | if (row.provider !== 'other') { 645 | throw new error.ValidationError('Cannot upload certificates for this type of provider'); 646 | } 647 | 648 | return internalCertificate.validate(data) 649 | .then((validations) => { 650 | if (typeof validations.certificate === 'undefined') { 651 | throw new error.ValidationError('Certificate file was not provided'); 652 | } 653 | 654 | _.map(data.files, (file, name) => { 655 | if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) { 656 | row.meta[name] = file.data.toString(); 657 | } 658 | }); 659 | 660 | // TODO: This uses a mysql only raw function that won't translate to postgres 661 | return internalCertificate.update(access, { 662 | id: data.id, 663 | expires_on: moment(validations.certificate.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss'), 664 | domain_names: [validations.certificate.cn], 665 | meta: _.clone(row.meta) // Prevent the update method from changing this value that we'll use later 666 | }) 667 | .then((certificate) => { 668 | certificate.meta = row.meta; 669 | return internalCertificate.writeCustomCert(certificate); 670 | }); 671 | }) 672 | .then(() => { 673 | return _.pick(row.meta, internalCertificate.allowedSslFiles); 674 | }); 675 | }); 676 | }, 677 | 678 | /** 679 | * Uses the openssl command to validate the private key. 680 | * It will save the file to disk first, then run commands on it, then delete the file. 681 | * 682 | * @param {String} private_key This is the entire key contents as a string 683 | */ 684 | checkPrivateKey: (private_key) => { 685 | return tempWrite(private_key, '/tmp') 686 | .then((filepath) => { 687 | return new Promise((resolve, reject) => { 688 | const failTimeout = setTimeout(() => { 689 | reject(new error.ValidationError('Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.')); 690 | }, 10000); 691 | utils 692 | .exec('openssl pkey -in ' + filepath + ' -check -noout 2>&1 ') 693 | .then((result) => { 694 | clearTimeout(failTimeout); 695 | if (!result.toLowerCase().includes('key is valid')) { 696 | reject(new error.ValidationError('Result Validation Error: ' + result)); 697 | } 698 | fs.unlinkSync(filepath); 699 | resolve(true); 700 | }) 701 | .catch((err) => { 702 | clearTimeout(failTimeout); 703 | fs.unlinkSync(filepath); 704 | reject(new error.ValidationError('Certificate Key is not valid (' + err.message + ')', err)); 705 | }); 706 | }); 707 | }); 708 | }, 709 | 710 | /** 711 | * Uses the openssl command to both validate and get info out of the certificate. 712 | * It will save the file to disk first, then run commands on it, then delete the file. 713 | * 714 | * @param {String} certificate This is the entire cert contents as a string 715 | * @param {Boolean} [throw_expired] Throw when the certificate is out of date 716 | */ 717 | getCertificateInfo: (certificate, throw_expired) => { 718 | return tempWrite(certificate, '/tmp') 719 | .then((filepath) => { 720 | return internalCertificate.getCertificateInfoFromFile(filepath, throw_expired) 721 | .then((certData) => { 722 | fs.unlinkSync(filepath); 723 | return certData; 724 | }).catch((err) => { 725 | fs.unlinkSync(filepath); 726 | throw err; 727 | }); 728 | }); 729 | }, 730 | 731 | /** 732 | * Uses the openssl command to both validate and get info out of the certificate. 733 | * It will save the file to disk first, then run commands on it, then delete the file. 734 | * 735 | * @param {String} certificate_file The file location on disk 736 | * @param {Boolean} [throw_expired] Throw when the certificate is out of date 737 | */ 738 | getCertificateInfoFromFile: (certificate_file, throw_expired) => { 739 | let certData = {}; 740 | 741 | return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout') 742 | .then((result) => { 743 | // subject=CN = something.example.com 744 | const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim; 745 | const match = regex.exec(result); 746 | 747 | if (typeof match[1] === 'undefined') { 748 | throw new error.ValidationError('Could not determine subject from certificate: ' + result); 749 | } 750 | 751 | certData['cn'] = match[1]; 752 | }) 753 | .then(() => { 754 | return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout'); 755 | }) 756 | .then((result) => { 757 | // issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3 758 | const regex = /^(?:issuer=)?(.*)$/gim; 759 | const match = regex.exec(result); 760 | 761 | if (typeof match[1] === 'undefined') { 762 | throw new error.ValidationError('Could not determine issuer from certificate: ' + result); 763 | } 764 | 765 | certData['issuer'] = match[1]; 766 | }) 767 | .then(() => { 768 | return utils.exec('openssl x509 -in ' + certificate_file + ' -dates -noout'); 769 | }) 770 | .then((result) => { 771 | // notBefore=Jul 14 04:04:29 2018 GMT 772 | // notAfter=Oct 12 04:04:29 2018 GMT 773 | let validFrom = null; 774 | let validTo = null; 775 | 776 | const lines = result.split('\n'); 777 | lines.map(function (str) { 778 | const regex = /^(\S+)=(.*)$/gim; 779 | const match = regex.exec(str.trim()); 780 | 781 | if (match && typeof match[2] !== 'undefined') { 782 | const date = parseInt(moment(match[2], 'MMM DD HH:mm:ss YYYY z').format('X'), 10); 783 | 784 | if (match[1].toLowerCase() === 'notbefore') { 785 | validFrom = date; 786 | } else if (match[1].toLowerCase() === 'notafter') { 787 | validTo = date; 788 | } 789 | } 790 | }); 791 | 792 | if (!validFrom || !validTo) { 793 | throw new error.ValidationError('Could not determine dates from certificate: ' + result); 794 | } 795 | 796 | if (throw_expired && validTo < parseInt(moment().format('X'), 10)) { 797 | throw new error.ValidationError('Certificate has expired'); 798 | } 799 | 800 | certData['dates'] = { 801 | from: validFrom, 802 | to: validTo 803 | }; 804 | 805 | return certData; 806 | }).catch((err) => { 807 | throw new error.ValidationError('Certificate is not valid (' + err.message + ')', err); 808 | }); 809 | }, 810 | 811 | /** 812 | * Cleans the ssl keys from the meta object and sets them to "true" 813 | * 814 | * @param {Object} meta 815 | * @param {Boolean} [remove] 816 | * @returns {Object} 817 | */ 818 | cleanMeta: function (meta, remove) { 819 | internalCertificate.allowedSslFiles.map((key) => { 820 | if (typeof meta[key] !== 'undefined' && meta[key]) { 821 | if (remove) { 822 | delete meta[key]; 823 | } else { 824 | meta[key] = true; 825 | } 826 | } 827 | }); 828 | 829 | return meta; 830 | }, 831 | 832 | /** 833 | * Request a certificate using the http challenge 834 | * @param {Object} certificate the certificate row 835 | * @returns {Promise} 836 | */ 837 | requestLetsEncryptSsl: (certificate) => { 838 | logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); 839 | 840 | const cmd = certbotCommand + ' certonly ' + 841 | '--config "' + letsencryptConfig + '" ' + 842 | '--work-dir "/tmp/letsencrypt-lib" ' + 843 | '--logs-dir "/tmp/letsencrypt-log" ' + 844 | '--cert-name "npm-' + certificate.id + '" ' + 845 | '--agree-tos ' + 846 | '--authenticator webroot ' + 847 | '--email "' + le_mail + '" ' + 848 | '--preferred-challenges "dns,http" ' + 849 | '--domains "' + certificate.domain_names.join(',') + '" ' + 850 | (letsencryptStaging ? '--staging' : ''); 851 | 852 | logger.info('Command:', cmd); 853 | 854 | return utils.exec(cmd) 855 | .then((result) => { 856 | logger.success(result); 857 | return result; 858 | }); 859 | }, 860 | 861 | /** 862 | * @param {Object} certificate the certificate row 863 | * @param {String} dns_provider the dns provider name (key used in `certbot-dns-plugins.js`) 864 | * @param {String | null} credentials the content of this providers credentials file 865 | * @param {String} propagation_seconds the cloudflare api token 866 | * @returns {Promise} 867 | */ 868 | requestLetsEncryptSslWithDnsChallenge: (certificate) => { 869 | const dns_plugin = dnsPlugins[certificate.meta.dns_provider]; 870 | 871 | if (!dns_plugin) { 872 | throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); 873 | } 874 | 875 | logger.info(`Requesting Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); 876 | 877 | const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id; 878 | // Escape single quotes and backslashes 879 | const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll('\'', '\\\'').replaceAll('\\', '\\\\'); 880 | const credentialsCmd = 'mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + escapedCredentials + '\' > \'' + credentialsLocation + '\' && chmod 600 \'' + credentialsLocation + '\''; 881 | // we call `. /opt/certbot/bin/activate` (`.` is alternative to `source` in dash) to access certbot venv 882 | const prepareCmd = '. /opt/certbot/bin/activate && pip install --no-cache-dir --user ' + dns_plugin.package_name + (dns_plugin.version_requirement || '') + ' ' + dns_plugin.dependencies + ' && deactivate'; 883 | 884 | // Whether the plugin has a ---credentials argument 885 | const hasConfigArg = certificate.meta.dns_provider !== 'route53'; 886 | 887 | let mainCmd = certbotCommand + ' certonly ' + 888 | '--config "' + letsencryptConfig + '" ' + 889 | '--work-dir "/tmp/letsencrypt-lib" ' + 890 | '--logs-dir "/tmp/letsencrypt-log" ' + 891 | '--cert-name "npm-' + certificate.id + '" ' + 892 | '--agree-tos ' + 893 | '--email "' + le_mail + '" ' + 894 | '--domains "' + certificate.domain_names.join(',') + '" ' + 895 | '--authenticator ' + dns_plugin.full_plugin_name + ' ' + 896 | ( 897 | hasConfigArg 898 | ? '--' + dns_plugin.full_plugin_name + '-credentials "' + credentialsLocation + '"' 899 | : '' 900 | ) + 901 | ( 902 | certificate.meta.propagation_seconds !== undefined 903 | ? ' --' + dns_plugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds 904 | : '' 905 | ) + 906 | (letsencryptStaging ? ' --staging' : ''); 907 | 908 | // Prepend the path to the credentials file as an environment variable 909 | if (certificate.meta.dns_provider === 'route53') { 910 | mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd; 911 | } 912 | 913 | logger.info('Command:', `${credentialsCmd} && ${prepareCmd} && ${mainCmd}`); 914 | 915 | return utils.exec(credentialsCmd) 916 | .then(() => { 917 | return utils.exec(prepareCmd) 918 | .then(() => { 919 | return utils.exec(mainCmd) 920 | .then(async (result) => { 921 | logger.info(result); 922 | return result; 923 | }); 924 | }); 925 | }).catch(async (err) => { 926 | // Don't fail if file does not exist 927 | const delete_credentialsCmd = `rm -f '${credentialsLocation}' || true`; 928 | await utils.exec(delete_credentialsCmd); 929 | throw err; 930 | }); 931 | }, 932 | 933 | 934 | /** 935 | * @param {Access} access 936 | * @param {Object} data 937 | * @param {Number} data.id 938 | * @returns {Promise} 939 | */ 940 | renew: (access, data) => { 941 | return access.can('certificates:update', data) 942 | .then(() => { 943 | return internalCertificate.get(access, data); 944 | }) 945 | .then((certificate) => { 946 | if (certificate.provider === 'letsencrypt') { 947 | const renewMethod = certificate.meta.dns_challenge ? internalCertificate.renewLetsEncryptSslWithDnsChallenge : internalCertificate.renewLetsEncryptSsl; 948 | 949 | return renewMethod(certificate) 950 | .then(() => { 951 | return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem'); 952 | }) 953 | .then((cert_info) => { 954 | return certificateModel 955 | .query() 956 | .patchAndFetchById(certificate.id, { 957 | expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss') 958 | }); 959 | }) 960 | .then((updated_certificate) => { 961 | // Add to audit log 962 | return internalAuditLog.add(access, { 963 | action: 'renewed', 964 | object_type: 'certificate', 965 | object_id: updated_certificate.id, 966 | meta: updated_certificate 967 | }) 968 | .then(() => { 969 | return updated_certificate; 970 | }); 971 | }); 972 | } else { 973 | throw new error.ValidationError('Only Let\'sEncrypt certificates can be renewed'); 974 | } 975 | }); 976 | }, 977 | 978 | /** 979 | * @param {Object} certificate the certificate row 980 | * @returns {Promise} 981 | */ 982 | renewLetsEncryptSsl: (certificate) => { 983 | logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); 984 | 985 | const cmd = certbotCommand + ' renew --force-renewal ' + 986 | '--config "' + letsencryptConfig + '" ' + 987 | '--work-dir "/tmp/letsencrypt-lib" ' + 988 | '--logs-dir "/tmp/letsencrypt-log" ' + 989 | '--cert-name "npm-' + certificate.id + '" ' + 990 | '--preferred-challenges "dns,http" ' + 991 | '--no-random-sleep-on-renew ' + 992 | '--disable-hook-validation ' + 993 | (letsencryptStaging ? '--staging' : ''); 994 | 995 | logger.info('Command:', cmd); 996 | 997 | return utils.exec(cmd) 998 | .then((result) => { 999 | logger.info(result); 1000 | return result; 1001 | }); 1002 | }, 1003 | 1004 | /** 1005 | * @param {Object} certificate the certificate row 1006 | * @returns {Promise} 1007 | */ 1008 | renewLetsEncryptSslWithDnsChallenge: (certificate) => { 1009 | const dns_plugin = dnsPlugins[certificate.meta.dns_provider]; 1010 | 1011 | if (!dns_plugin) { 1012 | throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); 1013 | } 1014 | 1015 | logger.info(`Renewing Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); 1016 | 1017 | let mainCmd = certbotCommand + ' renew ' + 1018 | '--config "' + letsencryptConfig + '" ' + 1019 | '--work-dir "/tmp/letsencrypt-lib" ' + 1020 | '--logs-dir "/tmp/letsencrypt-log" ' + 1021 | '--cert-name "npm-' + certificate.id + '" ' + 1022 | '--disable-hook-validation ' + 1023 | '--no-random-sleep-on-renew ' + 1024 | (letsencryptStaging ? ' --staging' : ''); 1025 | 1026 | // Prepend the path to the credentials file as an environment variable 1027 | if (certificate.meta.dns_provider === 'route53') { 1028 | const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id; 1029 | mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd; 1030 | } 1031 | 1032 | logger.info('Command:', mainCmd); 1033 | 1034 | return utils.exec(mainCmd) 1035 | .then(async (result) => { 1036 | logger.info(result); 1037 | return result; 1038 | }); 1039 | }, 1040 | 1041 | /** 1042 | * @param {Object} certificate the certificate row 1043 | * @param {Boolean} [throw_errors] 1044 | * @returns {Promise} 1045 | */ 1046 | revokeLetsEncryptSsl: (certificate, throw_errors) => { 1047 | logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); 1048 | 1049 | const mainCmd = certbotCommand + ' revoke ' + 1050 | '--config "' + letsencryptConfig + '" ' + 1051 | '--cert-path "/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem" ' + 1052 | '--delete-after-revoke ' + 1053 | (letsencryptStaging ? '--staging' : ''); 1054 | 1055 | // Don't fail command if file does not exist 1056 | const delete_credentialsCmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`; 1057 | 1058 | logger.info('Command:', mainCmd + '; ' + delete_credentialsCmd); 1059 | 1060 | return utils.exec(mainCmd) 1061 | .then(async (result) => { 1062 | await utils.exec(delete_credentialsCmd); 1063 | logger.info(result); 1064 | return result; 1065 | }) 1066 | .catch((err) => { 1067 | logger.error(err.message); 1068 | 1069 | if (throw_errors) { 1070 | throw err; 1071 | } 1072 | }); 1073 | }, 1074 | 1075 | /** 1076 | * @param {Object} certificate 1077 | * @returns {Boolean} 1078 | */ 1079 | hasLetsEncryptSslCerts: (certificate) => { 1080 | const letsencryptPath = '/etc/letsencrypt/live/npm-' + certificate.id; 1081 | 1082 | return fs.existsSync(letsencryptPath + '/fullchain.pem') && fs.existsSync(letsencryptPath + '/privkey.pem'); 1083 | }, 1084 | 1085 | /** 1086 | * @param {Object} in_use_result 1087 | * @param {Number} in_use_result.total_count 1088 | * @param {Array} in_use_result.proxy_hosts 1089 | * @param {Array} in_use_result.redirection_hosts 1090 | * @param {Array} in_use_result.dead_hosts 1091 | */ 1092 | disableInUseHosts: (in_use_result) => { 1093 | if (in_use_result.total_count) { 1094 | let promises = []; 1095 | 1096 | if (in_use_result.proxy_hosts.length) { 1097 | promises.push(internalNginx.bulkDeleteConfigs('proxy_host', in_use_result.proxy_hosts)); 1098 | } 1099 | 1100 | if (in_use_result.redirection_hosts.length) { 1101 | promises.push(internalNginx.bulkDeleteConfigs('redirection_host', in_use_result.redirection_hosts)); 1102 | } 1103 | 1104 | if (in_use_result.dead_hosts.length) { 1105 | promises.push(internalNginx.bulkDeleteConfigs('dead_host', in_use_result.dead_hosts)); 1106 | } 1107 | 1108 | return Promise.all(promises); 1109 | 1110 | } else { 1111 | return Promise.resolve(); 1112 | } 1113 | }, 1114 | 1115 | /** 1116 | * @param {Object} in_use_result 1117 | * @param {Number} in_use_result.total_count 1118 | * @param {Array} in_use_result.proxy_hosts 1119 | * @param {Array} in_use_result.redirection_hosts 1120 | * @param {Array} in_use_result.dead_hosts 1121 | */ 1122 | enableInUseHosts: (in_use_result) => { 1123 | if (in_use_result.total_count) { 1124 | let promises = []; 1125 | 1126 | if (in_use_result.proxy_hosts.length) { 1127 | promises.push(internalNginx.bulkGenerateConfigs('proxy_host', in_use_result.proxy_hosts)); 1128 | } 1129 | 1130 | if (in_use_result.redirection_hosts.length) { 1131 | promises.push(internalNginx.bulkGenerateConfigs('redirection_host', in_use_result.redirection_hosts)); 1132 | } 1133 | 1134 | if (in_use_result.dead_hosts.length) { 1135 | promises.push(internalNginx.bulkGenerateConfigs('dead_host', in_use_result.dead_hosts)); 1136 | } 1137 | 1138 | return Promise.all(promises); 1139 | 1140 | } else { 1141 | return Promise.resolve(); 1142 | } 1143 | }, 1144 | 1145 | testHttpsChallenge: async (access, domains) => { 1146 | await access.can('certificates:list'); 1147 | 1148 | if (!isArray(domains)) { 1149 | throw new error.InternalValidationError('Domains must be an array of strings'); 1150 | } 1151 | if (domains.length === 0) { 1152 | throw new error.InternalValidationError('No domains provided'); 1153 | } 1154 | 1155 | // Create a test challenge file 1156 | const testChallengeDir = '/data/letsencrypt-acme-challenge/.well-known/acme-challenge'; 1157 | const testChallengeFile = testChallengeDir + '/test-challenge'; 1158 | fs.mkdirSync(testChallengeDir, {recursive: true}); 1159 | fs.writeFileSync(testChallengeFile, 'Success', {encoding: 'utf8'}); 1160 | 1161 | async function performTestForDomain (domain) { 1162 | logger.info('Testing http challenge for ' + domain); 1163 | const url = `http://${domain}/.well-known/acme-challenge/test-challenge`; 1164 | const formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`; 1165 | const options = { 1166 | method: 'POST', 1167 | headers: { 1168 | 'Content-Type': 'application/x-www-form-urlencoded', 1169 | 'Content-Length': Buffer.byteLength(formBody) 1170 | } 1171 | }; 1172 | 1173 | const result = await new Promise((resolve) => { 1174 | 1175 | const req = https.request('https://www.site24x7.com/tools/restapi-tester', options, function (res) { 1176 | let responseBody = ''; 1177 | 1178 | res.on('data', (chunk) => responseBody = responseBody + chunk); 1179 | res.on('end', function () { 1180 | const parsedBody = JSON.parse(responseBody + ''); 1181 | if (res.statusCode !== 200) { 1182 | logger.warn(`Failed to test HTTP challenge for domain ${domain}`, res); 1183 | resolve(undefined); 1184 | } 1185 | resolve(parsedBody); 1186 | }); 1187 | }); 1188 | 1189 | // Make sure to write the request body. 1190 | req.write(formBody); 1191 | req.end(); 1192 | req.on('error', function (e) { logger.warn(`Failed to test HTTP challenge for domain ${domain}`, e); 1193 | resolve(undefined); }); 1194 | }); 1195 | 1196 | if (!result) { 1197 | // Some error occurred while trying to get the data 1198 | return 'failed'; 1199 | } else if (`${result.responsecode}` === '200' && result.htmlresponse === 'Success') { 1200 | // Server exists and has responded with the correct data 1201 | return 'ok'; 1202 | } else if (`${result.responsecode}` === '200') { 1203 | // Server exists but has responded with wrong data 1204 | logger.info(`HTTP challenge test failed for domain ${domain} because of invalid returned data:`, result.htmlresponse); 1205 | return 'wrong-data'; 1206 | } else if (`${result.responsecode}` === '404') { 1207 | // Server exists but responded with a 404 1208 | logger.info(`HTTP challenge test failed for domain ${domain} because code 404 was returned`); 1209 | return '404'; 1210 | } else if (`${result.responsecode}` === '0' || (typeof result.reason === 'string' && result.reason.toLowerCase() === 'host unavailable')) { 1211 | // Server does not exist at domain 1212 | logger.info(`HTTP challenge test failed for domain ${domain} the host was not found`); 1213 | return 'no-host'; 1214 | } else { 1215 | // Other errors 1216 | logger.info(`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`); 1217 | return `other:${result.responsecode}`; 1218 | } 1219 | } 1220 | 1221 | const results = {}; 1222 | 1223 | for (const domain of domains){ 1224 | results[domain] = await performTestForDomain(domain); 1225 | } 1226 | 1227 | // Remove the test challenge file 1228 | fs.unlinkSync(testChallengeFile); 1229 | 1230 | return results; 1231 | } 1232 | }; 1233 | 1234 | module.exports = internalCertificate; 1235 | -------------------------------------------------------------------------------- /library/README.md: -------------------------------------------------------------------------------- 1 | # ansible-nginx-proxy-manager 2 | 3 | A simple way to add a new proxy host or to delete via ansible playbook. 4 | 5 | ## Requirements 6 | `pip install requests` 7 | 8 | ## Module Options 9 | | Parameter | Required | Choices | Default | Comments | 10 | | --- | --- | --- | --- | --- | 11 | | url | Y | | | URL for the Nginx Proxy Manager REST API 12 | | token | Y | | | Tokens are required to authenticate against the API 13 | | domain | Y | | | Domain Names 14 | | host | Y | | | Forward Hostname / IP 15 | | host_port | N | | 80 | Forward Port 16 | | ssl_forced | N | True/False | True | Is SSL Forced? 17 | | state | N | present, absent | present | Whether to create (present), or remove (absent) a proxy host. 18 | 19 | 20 | ## Examples 21 | ```yaml 22 | name: Create Proxy-Host an NPM 23 | npm_proxy: 24 | url: "http://192.168.0.1:81/api" 25 | token: "npm_access_token" 26 | domain: "domain_name.example.com" 27 | host: "172.32.0.1" 28 | ssl_forced: True 29 | state: present 30 | 31 | name: Delete Proxy-Host an NPM 32 | npm_proxy: 33 | url: "http://192.168.0.1:81/api" 34 | token: "npm_access_token" 35 | domain: "domain_name.example.com" 36 | host: "172.32.0.1" 37 | state: absent 38 | ``` 39 | 40 | The given code is an Ansible module called nginx-proxy-manager-ansible. The module allows adding, removing or deleting a new proxy host through an Ansible playbook. The module takes in parameters such as the url of the Nginx Proxy Manager REST API, tokens to authenticate, domain names, forward hostname / IP, forward port, SSL forcing, and state (present or absent). 41 | 42 | 43 | The module then first checks if the proxy-host already exists, if it does not exist, it creates a new proxy host based on the given parameters. If the state is absent, then the module deletes the proxy host using the given domain name. 44 | 45 | 46 | The module uses REST APIs to create or delete a proxy host or to search through certificates. The module makes HTTP requests with headers that contain the tokens to authenticate against the API. 47 | 48 | 49 | The code seems well documented with a good description of the options, examples, and return values. The code uses the AnsibleModule class, which is an Ansible built-in utility that simplifies module writing. Overall, the code seems well written and should function as expected. -------------------------------------------------------------------------------- /library/npm_proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2022, DenAV https://github.com/DenAV 5 | from __future__ import absolute_import, division, print_function 6 | __metaclass__ = type 7 | 8 | DOCUMENTATION = r''' 9 | --- 10 | module: nginx-proxy-manager-ansible 11 | 12 | short_description: a simple way to add a new proxy host via ansible playbook 13 | 14 | version_added: "1.0.0" 15 | 16 | description: a simple way to add a new proxy host or to delete via ansible playbook 17 | 18 | options: 19 | url: 20 | description: IP for the Nginx Proxy Manager REST API 21 | required: true 22 | type: str 23 | token: 24 | description: Tokens are required to authenticate against the API 25 | required: true 26 | type: str 27 | domain: 28 | description: Domain Names 29 | required: true 30 | type: str 31 | host: 32 | description: Forward Hostname / IP 33 | required: true 34 | type: str 35 | host_port: 36 | description: Forward Port 37 | required: false 38 | default: 80 39 | type: int 40 | ssl_forced: 41 | description: Is SSL Forced? 42 | required: false 43 | default: true 44 | type: bool 45 | state: 46 | description: Whether to create (present), or remove (absent) a proxy host. 47 | required: false 48 | type: str 49 | choices=['absent', 'present'] 50 | ''' 51 | EXAMPLES = r''' 52 | # create new proxy host 53 | - name: Create Proxy-Host an NPM 54 | npm_proxy: 55 | url: "http://192.168.0.1:81/api" 56 | token: "npm_access_token" 57 | domain: "domain_name.example.com" 58 | host: "172.32.0.1" 59 | ssl_forced: True 60 | state: present 61 | 62 | # delete proxy host 63 | - name: Create Proxy-Host an NPM 64 | npm_proxy: 65 | url: "http://192.168.0.1:81/api" 66 | token: "npm_access_token" 67 | domain: "domain_name.example.com" 68 | host: "172.32.0.1" 69 | state: absent 70 | ''' 71 | RETURN = r''' 72 | # The return information. 73 | msg: 74 | description: The output message that the nginx-proxy-manager-ansible module generates. 75 | returned: always 76 | type: str 77 | sample: "domain_name.example.com" 78 | ''' 79 | 80 | # Install the Python Requests library: 81 | # `pip install requests` 82 | 83 | import requests 84 | import json 85 | 86 | from ansible.module_utils.basic import AnsibleModule 87 | from ansible.module_utils.urls import fetch_url 88 | 89 | def build_url(api_url, action, item_id=None): 90 | if action == "create-host": 91 | return "%s/nginx/proxy-hosts" % api_url, "POST" 92 | elif action == "search-host": 93 | return "%s/nginx/proxy-hosts" % api_url, "GET" 94 | elif action == "delete-host": 95 | return "%s/nginx/proxy-hosts/%s" % (api_url, item_id), "DELETE" 96 | elif action == "create-ssl": 97 | return "%s/nginx/certificates" % api_url, "POST" 98 | elif action == "search-ssl": 99 | return "%s/nginx/certificates" % api_url, "GET" 100 | elif action == "delete-ssl": 101 | return "%s/nginx/certificates/%s" % (api_url, item_id), "DELETE" 102 | 103 | def http_request(api_url, token, action, data=None, item_id=None): 104 | 105 | if item_id is None: 106 | url, method = build_url(api_url, action) 107 | else: 108 | url, method = build_url(api_url, action, item_id) 109 | 110 | headers = dict() 111 | headers["Authorization"] = "Bearer %s" % token 112 | headers["Content-Type"] = "application/json" 113 | 114 | if method == "GET": 115 | response = requests.get(url=url, data=data, headers=headers) 116 | elif method == "POST": 117 | response = requests.post(url=url, data=data, headers=headers) 118 | elif method == "DELETE": 119 | response = requests.delete(url=url, data=data, headers=headers) 120 | 121 | return response, response.status_code 122 | 123 | def search_proxy_host(module, api_url, token, domain_name): 124 | response, info = http_request(api_url, token, action="search-host") 125 | 126 | status_code = info 127 | if status_code >= 400: 128 | module.fail_json("Failed to connect to api host to search for proxy_host. Info: %s" % response) 129 | 130 | result_search = "" 131 | for search in json.loads(response.text): 132 | if domain_name in search["domain_names"]: 133 | result_search = search 134 | 135 | # Return proxy_host 136 | return result_search 137 | 138 | def create_proxy_host(module, api_url, token, domain_name, forward_host, forward_port, ssl_forced): 139 | # Create Proxy-host 140 | 141 | proxy_host = search_proxy_host(module, api_url, token, domain_name) 142 | 143 | if len(proxy_host) > 0: 144 | # If the Proxy-host already exists, do nothing 145 | return 0, "Proxy Host %s already exists" % domain_name 146 | 147 | else: 148 | forward_scheme = "http" 149 | 150 | if ssl_forced: 151 | data_request = json.dumps({ 152 | "domain_names": [domain_name], 153 | "forward_host": forward_host, 154 | "forward_port": forward_port, 155 | "forward_scheme": forward_scheme, 156 | "certificate_id": "new", 157 | "ssl_forced": ssl_forced, 158 | "allow_websocket_upgrade": True, 159 | }) 160 | else: 161 | data_request = json.dumps({ 162 | "domain_names": [domain_name], 163 | "forward_host": forward_host, 164 | "forward_port": forward_port, 165 | "forward_scheme": forward_scheme, 166 | }) 167 | 168 | response, info = http_request(api_url, token, data=data_request, action="create-host") 169 | 170 | status_code = info 171 | if status_code == 201: 172 | return 1, "Proxy-host %s created" % domain_name 173 | 174 | elif status_code >= 400: 175 | return 2, "Failed to connect to api host to create for proxy_host. Info: %s" % response 176 | 177 | def delete_proxy_host(module, api_url, token, domain_name): 178 | # Delete Proxy-host 179 | 180 | proxy_host = search_proxy_host(module, api_url, token, domain_name) 181 | 182 | if len(proxy_host) > 0: 183 | # If the Proxy-host already exists, do remove 184 | if proxy_host['certificate_id'] > 0: 185 | # IF the Proxy-host have certificate 186 | rc, result = delete_certificate(module, api_url, token, item_id=proxy_host['certificate_id']) 187 | 188 | if rc == 0 or rc == 1: 189 | response, status_code = http_request(api_url, token, item_id=proxy_host['id'], action="delete-host") 190 | 191 | if status_code == 200: 192 | return 1, "Proxy-host and certificate: %s remowed." % domain_name 193 | 194 | elif status_code >= 400: 195 | return 2, "Failed to delete for Proxy-host and certificate: %s. Info: %s" % (domain_name, response) 196 | else: 197 | return 2, "Failed to delete for Proxy-host and certificate: %s. Info: %s" % (domain_name, result) 198 | 199 | else: 200 | response, status_code = http_request(api_url, token, item_id=proxy_host['id'], action="delete-host") 201 | 202 | if status_code == 200: 203 | return 1, "Proxy-host: %s remowed." % domain_name 204 | 205 | elif status_code >= 400: 206 | return 2, "Failed to delete for Proxy-host: %s. Info: %s" % (domain_name, response) 207 | 208 | else: 209 | return 0, "Proxy-host " + domain_name + " already deleted." 210 | 211 | def search_certificate(module, api_url, token, domain_name=None, item_id=None): 212 | response, info = http_request(api_url, token, action="search-ssl") 213 | 214 | status_code = info 215 | if status_code >= 400: 216 | module.fail_json("Failed to search for certificate. Info: %s" % response) 217 | 218 | result_search = "" 219 | if domain_name is not None: 220 | for search in json.loads(response.text): 221 | if domain_name in search["domain_names"]: 222 | result_search = search 223 | 224 | elif item_id is not None: 225 | for search in json.loads(response.text): 226 | if item_id == search["id"]: 227 | result_search = search 228 | 229 | # Return certificate 230 | return result_search 231 | 232 | def delete_certificate(module, api_url, token, item_id): 233 | 234 | certificate = search_certificate(module, api_url, token, item_id=item_id) 235 | 236 | if len(certificate) > 0: 237 | # If the certificate already exists, do remove 238 | response, info = http_request(api_url, token, item_id=item_id, action="delete-ssl") 239 | 240 | status_code = info 241 | if status_code == 200: 242 | result = "Certificate id: %s remowed" % item_id 243 | return 1, result 244 | 245 | elif status_code >= 400: 246 | result = "Failed to delete for certificate id: %s. Info: %s" % (item_id, response) 247 | return 2, result 248 | 249 | else: 250 | result = "Certificate id: %s does not exist." % item_id 251 | return 0, result 252 | 253 | def main(): 254 | module = AnsibleModule( 255 | argument_spec=dict( 256 | url=dict(type='str', required=True), 257 | token=dict(type='str', required=True, no_log=True), 258 | domain=dict(type='str', required=True), 259 | host=dict(type='str', required=True), 260 | host_port=dict(type='int', required=False, default=80), 261 | ssl_forced=dict(type='bool', required=False, default=True), 262 | state=dict(type='str', default='present', choices=['absent', 'present']), 263 | ), 264 | ) 265 | 266 | api_url = module.params['url'] 267 | token = module.params['token'] 268 | domain_name = module.params['domain'] 269 | forward_host = module.params['host'] 270 | forward_port = module.params['host_port'] 271 | ssl_forced = module.params['ssl_forced'] 272 | state = module.params['state'] 273 | 274 | if state == 'present': 275 | (rc, result) = create_proxy_host(module, api_url, token, domain_name, forward_host, forward_port, ssl_forced) 276 | elif state == 'absent': 277 | (rc, result) = delete_proxy_host(module, api_url, token, domain_name) 278 | 279 | if rc == 2: 280 | module.fail_json(msg=result) 281 | elif rc == 1: 282 | module.exit_json(msg=result, changed=True) 283 | else: 284 | module.exit_json(msg=result, changed=False) 285 | 286 | if __name__ == '__main__': 287 | main() 288 | -------------------------------------------------------------------------------- /pl_npm-management.yml: -------------------------------------------------------------------------------- 1 | ## Add a new proxy host 2 | - name: NPM - create proxy host 3 | hosts: localhost 4 | gather_facts: no 5 | 6 | roles: 7 | - role: npm-management 8 | npm_api_domain_name: "site-2.example.com" 9 | npm_api_host: "172.16.1.2" 10 | npm_api_ssl_forced: True 11 | npm_api_create_host: True 12 | -------------------------------------------------------------------------------- /roles/npm-management/.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: python 3 | python: "2.7" 4 | 5 | # Use the new container infrastructure 6 | sudo: false 7 | 8 | # Install ansible 9 | addons: 10 | apt: 11 | packages: 12 | - python-pip 13 | 14 | install: 15 | # Install ansible 16 | - pip install ansible 17 | 18 | # Check ansible version 19 | - ansible --version 20 | 21 | # Create ansible.cfg with correct roles_path 22 | - printf '[defaults]\nroles_path=../' >ansible.cfg 23 | 24 | script: 25 | # Basic role syntax check 26 | - ansible-playbook tests/test.yml -i tests/inventory --syntax-check 27 | 28 | notifications: 29 | webhooks: https://galaxy.ansible.com/api/v1/notifications/ -------------------------------------------------------------------------------- /roles/npm-management/README.md: -------------------------------------------------------------------------------- 1 | ## NPM-MANAGEMENT 2 | ## Ansible role for [Nginx Proxy Manager v2.10.3](https://github.com/NginxProxyManager/nginx-proxy-manager/tree/v2.10.3). 3 | a simple way to add a new proxy host via ansible playbook. 4 | Checked for version v2.10.3. 5 | 6 | ========= 7 | 8 | description 9 | ----------- 10 | 11 | a simple way to add a new proxy host or to delete via ansible playbook 12 | 13 | Requirements 14 | ------------ 15 | 16 | This role requires Ansible 2.7 or higher, Docker and Docker-Compose. 17 | 18 | Change and update a [docker-compose.yml](https://github.com/DenAV/nginx-proxy-manager-ansible/blob/main/docker/docker-compose_npm.yml) file. Bring up your stack by running docker-compose, further info [here](https://github.com/DenAV/nginx-proxy-manager-ansible/tree/main/docker). 19 | 20 | Role Variables 21 | -------------- 22 | 23 | - `npm_api_url` - IP for the Nginx Proxy Manager REST API. Default to `http://192.168.1.5:81/api`. 24 | - `npm_user` - User to authenticate the Nginx Proxy Manager REST API. 25 | - `npm_password` - Password to authenticate the Nginx Proxy Manager REST API. 26 | - `npm_access_token` - Tokens are required to authenticate against the API. 27 | 28 | - `npm_api_domain_name` - Domain Names are required to create the Proxy host. 29 | - `npm_api_host` - Forward Hostname / IP are required to create the Proxy host. 30 | - `npm_api_ssl_forced` - Is SSL Forced? Default is `False`. 31 | - `npm_api_create_host` - IWhether to create (present), or no a proxy host. Default is `False`. 32 | 33 | See the [`defaults/main.yml`](https://github.com/DenAV/nginx-proxy-manager-ansible/blob/main/roles/npm-management/defaults/main.yml) or [`vars/*.yml`](https://github.com/DenAV/nginx-proxy-manager-ansible/tree/main/roles/npm-management/vars) file listing all possible options which you can be passed to a runner registration command. 34 | 35 | 36 | Dependencies 37 | ------------ 38 | 39 | A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. 40 | 41 | Example Playbook 42 | ---------------- 43 | 44 | Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: 45 | 46 | ```yaml 47 | - name: NPM - create proxy host 48 | hosts: localhost 49 | gather_facts: no 50 | 51 | roles: 52 | - role: npm-management 53 | npm_api_domain_name: "site-2.example.com" 54 | npm_api_host: "172.16.1.2" 55 | npm_api_ssl_forced: True 56 | npm_api_create_host: True 57 | ``` 58 | 59 | License 60 | ------- 61 | 62 | BSD 63 | 64 | Author Information 65 | ------------------ 66 | 67 | https://github.com/DenAV 68 | 69 | 70 | -------------------------------------------------------------------------------- /roles/npm-management/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults file for npm-management 3 | 4 | npm_api_create_host: False 5 | npm_api_ssl_forced: False 6 | 7 | npm_api_domain_name: "" 8 | npm_api_host: "" 9 | -------------------------------------------------------------------------------- /roles/npm-management/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # handlers file for npm-management 3 | -------------------------------------------------------------------------------- /roles/npm-management/meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | author: your name 3 | description: your role description 4 | company: your company (optional) 5 | 6 | # If the issue tracker for your role is not on github, uncomment the 7 | # next line and provide a value 8 | # issue_tracker_url: http://example.com/issue/tracker 9 | 10 | # Choose a valid license ID from https://spdx.org - some suggested licenses: 11 | # - BSD-3-Clause (default) 12 | # - MIT 13 | # - GPL-2.0-or-later 14 | # - GPL-3.0-only 15 | # - Apache-2.0 16 | # - CC-BY-4.0 17 | license: license (GPL-2.0-or-later, MIT, etc) 18 | 19 | min_ansible_version: 2.1 20 | 21 | # If this a Container Enabled role, provide the minimum Ansible Container version. 22 | # min_ansible_container_version: 23 | 24 | # 25 | # Provide a list of supported platforms, and for each platform a list of versions. 26 | # If you don't wish to enumerate all versions for a particular platform, use 'all'. 27 | # To view available platforms and versions (or releases), visit: 28 | # https://galaxy.ansible.com/api/v1/platforms/ 29 | # 30 | # platforms: 31 | # - name: Fedora 32 | # versions: 33 | # - all 34 | # - 25 35 | # - name: SomePlatform 36 | # versions: 37 | # - all 38 | # - 1.0 39 | # - 7 40 | # - 99.99 41 | 42 | galaxy_tags: [] 43 | # List tags for your role here, one per line. A tag is a keyword that describes 44 | # and categorizes the role. Users find roles by searching for tags. Be sure to 45 | # remove the '[]' above, if you add tags to this list. 46 | # 47 | # NOTE: A tag is limited to a single word comprised of alphanumeric characters. 48 | # Maximum 20 tags per role. 49 | 50 | dependencies: [] 51 | # List your role dependencies here, one per line. Be sure to remove the '[]' above, 52 | # if you add dependencies to this list. 53 | -------------------------------------------------------------------------------- /roles/npm-management/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks file for npm-managenment 3 | - name: "Include OS-specific variables" 4 | include_vars: 5 | dir: vars 6 | no_log: True 7 | 8 | # --- API Heath-Check-Endpunkt 9 | - name: NPM | API Heath-Check-Endpunkt 10 | uri: 11 | url: "{{npm_api_check_url}}" 12 | timeout: 1 13 | return_content: yes 14 | register: npm_api_check_result 15 | changed_when: no 16 | check_mode: no 17 | failed_when: no 18 | delegate_to: localhost 19 | no_log: True 20 | 21 | # --- Get Access Token --- 22 | - name: NPM Post | Get Access Token 23 | uri: 24 | url: '{{npm_api_url}}/tokens' 25 | method: POST 26 | validate_certs: no 27 | status_code: 200 28 | body_format: json 29 | body: 30 | identity: "{{npm_user}}" 31 | secret: "{{npm_password}}" 32 | headers: 33 | Content-Type: application/json 34 | register: npm_access_token 35 | delegate_to: localhost 36 | no_log: True 37 | when: 38 | - npm_api_check_result.status == 200 39 | 40 | - name: Create Proxy-Host an NPM 41 | npm_proxy: 42 | url: "{{npm_api_url}}" 43 | token: "{{npm_access_token.json.token}}" 44 | domain: "{{npm_api_domain_name}}" 45 | host: "{{npm_api_host}}" 46 | state: present 47 | ssl_forced: "{{npm_api_ssl_forced}}" 48 | delegate_to: localhost 49 | throttle: 1 50 | when: 51 | - npm_access_token 52 | - npm_api_domain_name is defined 53 | - npm_api_create_host == True 54 | 55 | -------------------------------------------------------------------------------- /roles/npm-management/vars/api_secret.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # API Secter 3 | npm_user: npm-manager@example.com 4 | npm_password: password 5 | -------------------------------------------------------------------------------- /roles/npm-management/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # vars file for npm-managenment 3 | npm_api_url: http://192.168.1.5:81/api 4 | npm_api_check_url: "{{npm_api_url}}" 5 | --------------------------------------------------------------------------------