diff --git a/infrastructure/create-github-environment.js b/infrastructure/create-github-environment.js index 06f2c9ce4..310724e4b 100644 --- a/infrastructure/create-github-environment.js +++ b/infrastructure/create-github-environment.js @@ -1,17 +1,27 @@ +const minimist = require('minimist') const sodium = require('libsodium-wrappers') const { Octokit } = require('@octokit/core') const { writeFileSync } = require('fs') +const { existsSync } = require('fs') +const { mkdirSync } = require('fs') + +const args = minimist(process.argv.slice(2), { + string: ['vpn-type'], + boolean: ['sms-enabled', 'configure-vpn', 'dry-run', 'configure-backup'], + alias: {} +}) const config = { environment: '', - repo: { - REPOSITORY_ID: '', - REPOSITORY_ACCOUNT: '', - REPOSITORY_NAME: '', - DOCKERHUB_ACCOUNT: '', // This may be a dockerhub organisation or the same as the username - DOCKERHUB_REPO: '', - DOCKER_USERNAME: process.env.DOCKER_USERNAME, - DOCKER_TOKEN: process.env.DOCKER_TOKEN + dockerhub: { + ORGANISATION: 'opencrvs', // This may be a dockerhub organisation or the same as the username + REPOSITORY: 'opencrvs-farajaland', + USERNAME: process.env.DOCKER_USERNAME, + TOKEN: process.env.DOCKER_TOKEN + }, + github_repository: { + ORGANISATION: 'opencrvs', + REPOSITORY_NAME: 'opencrvs-farajaland' }, ssh: { KNOWN_HOSTS: process.env.KNOWN_HOSTS, @@ -25,11 +35,13 @@ const config = { DOMAIN: '', // web domain applied after all public subdomains REPLICAS: '1' }, + sms: { + INFOBIP_API_KEY: process.env.INFOBIP_API_KEY, + INFOBIP_GATEWAY_ENDPOINT: process.env.INFOBIP_GATEWAY_ENDPOINT, + INFOBIP_SENDER_ID: process.env.INFOBIP_SENDER_ID // the name of the SMS sender e.g. OpenCRVS + }, services: { - SENTRY_DSN: process.env.SENTRY_DSN || '', - INFOBIP_API_KEY: process.env.INFOBIP_API_KEY || '', - INFOBIP_GATEWAY_ENDPOINT: process.env.INFOBIP_GATEWAY_ENDPOINT || '', - INFOBIP_SENDER_ID: process.env.INFOBIP_SENDER_ID || '' // the name of the SMS sender e.g. OpenCRVS + SENTRY_DSN: process.env.SENTRY_DSN }, seeding: { ACTIVATE_USERS: '', // Must be a string 'true' for QA or 'false' in PRODUCTION! @@ -38,21 +50,27 @@ const config = { GATEWAY_HOST: '' }, smtp: { - SMTP_HOST: process.env.SMTP_HOST || '', - SMTP_USERNAME: process.env.SMTP_USERNAME || '', - SMTP_PASSWORD: process.env.SMTP_PASSWORD || '', - EMAIL_API_KEY: process.env.EMAIL_API_KEY || '', + SMTP_HOST: process.env.SMTP_HOST, + SMTP_USERNAME: process.env.SMTP_USERNAME, + SMTP_PASSWORD: process.env.SMTP_PASSWORD, SMTP_PORT: '', ALERT_EMAIL: '' }, vpn: { // openconnect details for optional VPN - VPN_PROTOCOL: '', // e,g, fortinet, wireguard etc - VPN_HOST: process.env.VPN_HOST || '', - VPN_PORT: process.env.VPN_PORT || '', - VPN_USER: process.env.VPN_USER || '', - VPN_PWD: process.env.VPN_PWD || '', - VPN_SERVERCERT: process.env.VPN_SERVERCERT || '' + type: args['vpn-type'], // e,g, fortinet, wireguard etc + wireguard: { + VPN_HOST_ADDRESS: process.env.VPN_HOST_ADDRESS, // IP address for the VPN server + VPN_ADMIN_PASSWORD: process.env.VPN_ADMIN_PASSWORD + }, + openconnect: { + VPN_PROTOCOL: process.env.VPN_PROTOCOL, + VPN_HOST_ADDRESS: process.env.VPN_HOST_ADDRESS, + VPN_PORT: process.env.VPN_PORT, + VPN_USER: process.env.VPN_USER, + VPN_PWD: process.env.VPN_PWD, + VPN_SERVERCERT: process.env.VPN_SERVERCERT + } }, whitelist: { CONTENT_SECURITY_POLICY_WILDCARD: '*.', // e.g. *. @@ -69,11 +87,11 @@ const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }) -async function createVariable(environment, name, value) { +async function createVariable(repositoryId, environment, name, value) { await octokit.request( - `POST /repositories/${config.repo.REPOSITORY_ID}/environments/${config.environment}/variables`, + `POST /repositories/${repositoryId}/environments/${config.environment}/variables`, { - repository_id: config.repo.REPOSITORY_ID, + repository_id: repositoryId, environment_name: environment, name: name, value: value, @@ -84,7 +102,27 @@ async function createVariable(environment, name, value) { ) } -async function createSecret(environment, key, keyId, name, secret) { +async function getRepositoryId(owner, repo) { + try { + const response = await octokit.request('GET /repos/{owner}/{repo}', { + owner: owner, + repo: repo + }) + + return response.data.id + } catch (error) { + console.error('Error fetching repository information:', error) + } +} + +async function createSecret( + repositoryId, + environment, + key, + keyId, + name, + secret +) { //Check if libsodium is ready and then proceed. await sodium.ready @@ -102,9 +140,9 @@ async function createSecret(environment, key, keyId, name, secret) { ) await octokit.request( - `PUT /repositories/${config.repo.REPOSITORY_ID}/environments/${environment}/secrets/${name}`, + `PUT /repositories/${repositoryId}/environments/${environment}/secrets/${name}`, { - repository_id: config.repo.REPOSITORY_ID, + repository_id: repositoryId, environment_name: environment, secret_name: name, encrypted_value: encryptedValue, @@ -117,8 +155,12 @@ async function createSecret(environment, key, keyId, name, secret) { } async function getPublicKey(environment) { + const repositoryId = await getRepositoryId( + config.github_repository.ORGANISATION, + config.github_repository.REPOSITORY_NAME + ) await octokit.request( - `PUT /repos/${config.repo.REPOSITORY_ACCOUNT}/${config.repo.REPOSITORY_NAME}/environments/${environment}`, + `PUT /repos/${config.github_repository.ORGANISATION}/${config.github_repository.REPOSITORY_NAME}/environments/${environment}`, { headers: { 'X-GitHub-Api-Version': '2022-11-28' @@ -127,10 +169,10 @@ async function getPublicKey(environment) { ) const res = await octokit.request( - `GET /repositories/${config.repo.REPOSITORY_ID}/environments/${environment}/secrets/public-key`, + `GET /repositories/${repositoryId}/environments/${environment}/secrets/public-key`, { - owner: config.repo.DOCKERHUB_ACCOUNT, - repo: config.repo.DOCKERHUB_REPO, + owner: config.github_repository.ORGANISATION, + repo: config.github_repository.REPOSITORY_NAME, headers: { 'X-GitHub-Api-Version': '2022-11-28' } @@ -150,12 +192,23 @@ function generateLongPassword() { } async function main() { + if (!config.environment) { + console.error('Please specify an environment in config.environment') + process.exit(1) + } + const { key, key_id } = await getPublicKey(config.environment) + const repositoryId = await getRepositoryId( + config.github_repository.ORGANISATION, + config.github_repository.REPOSITORY_NAME + ) + let backupSecrets = {} let backupVariables = {} let vpnSecrets = {} + let smsSecrets = {} - if (process.argv.includes('--configure-backup')) { + if (args['configure-backup']) { backupSecrets = { BACKUP_HOST: config.backup.BACKUP_HOST } @@ -165,9 +218,19 @@ async function main() { } } - if (process.argv.includes('--configure-vpn')) { + if (args['configure-vpn']) { + if (!config.vpn.type) { + console.error('Please specify a VPN type with --vpn-type') + process.exit(1) + } vpnSecrets = { - ...config.vpn + ...config.vpn[config.vpn.type] + } + } + + if (args['sms-enabled']) { + smsSecrets = { + ...config.sms } } @@ -185,15 +248,16 @@ async function main() { } const SECRETS = { - DOCKERHUB_ACCOUNT: config.repo.DOCKERHUB_ACCOUNT, - DOCKERHUB_REPO: config.repo.DOCKERHUB_REPO, - DOCKER_TOKEN: config.repo.DOCKER_TOKEN, + DOCKERHUB_ACCOUNT: config.dockerhub.ORGANISATION, + DOCKERHUB_REPO: config.dockerhub.REPOSITORY, + DOCKER_TOKEN: config.github_repository.DOCKER_TOKEN, ...SECRETS_TO_SAVE_IN_PASSWORD_MANAGER, ...config.ssh, ...config.smtp, ...config.services, ...backupSecrets, - ...vpnSecrets + ...vpnSecrets, + ...smsSecrets } const VARIABLES = { ...config.infrastructure, @@ -201,30 +265,67 @@ async function main() { ...config.whitelist, ...backupVariables } - writeFileSync( - '../.secrets/SECRETS_TO_SAVE_IN_PASSWORD_MANAGER_FOR_ENV_' + - config.environment + - '.json', - JSON.stringify([SECRETS_TO_SAVE_IN_PASSWORD_MANAGER], null, 2) - ) - if (process.argv.includes('--dry-run')) { - console.log('Dry run. Not creating secrets or variables.') - process.exit(0) - } else { - for (const [secretName, secretValue] of Object.entries(SECRETS)) { - await createSecret( - config.environment, - key, - key_id, - secretName, - secretValue + + if (!existsSync('../.secrets')) { + mkdirSync('../.secrets') + } + + const errors = [] + for (const [secretName, secretValue] of Object.entries(SECRETS)) { + if (secretValue === undefined || secretValue === '') { + errors.push( + `Secret ${secretName} is empty. Please set the value in the config.` ) } + } - for (const [variableName, variableValue] of Object.entries(VARIABLES)) { - await createVariable(config.environment, variableName, variableValue) + for (const [variableName, variableValue] of Object.entries(VARIABLES)) { + if (variableValue === undefined || variableValue === '') { + errors.push( + `Variable ${variableName} is empty. Please set the value in the config.` + ) } } + + if (args['dry-run']) { + console.log('Dry run. Not creating secrets or variables.') + console.log(SECRETS) + console.log(VARIABLES) + console.log('Errors:', errors) + process.exit(0) + } + + if (errors.length > 0) { + console.error(errors) + process.exit(1) + } + + for (const [secretName, secretValue] of Object.entries(SECRETS)) { + await createSecret( + repositoryId, + config.environment, + key, + key_id, + secretName, + secretValue + ) + } + + for (const [variableName, variableValue] of Object.entries(VARIABLES)) { + await createVariable( + repositoryId, + config.environment, + variableName, + variableValue + ) + } + + writeFileSync( + '../.secrets/SECRETS_TO_SAVE_IN_PASSWORD_MANAGER_FOR_ENV_' + + config.environment + + '.json', + JSON.stringify([SECRETS_TO_SAVE_IN_PASSWORD_MANAGER], null, 2) + ) } main() diff --git a/infrastructure/docker-compose.deploy.yml b/infrastructure/docker-compose.deploy.yml index 8c1a6c033..7bd2a7ee1 100644 --- a/infrastructure/docker-compose.deploy.yml +++ b/infrastructure/docker-compose.deploy.yml @@ -15,12 +15,8 @@ services: traefik: image: 'traefik:v2.9' ports: - - target: 80 - published: 80 - mode: host - - target: 443 - published: 443 - mode: host + - '${VPN_HOST_ADDRESS:-0.0.0.0}:80:80' + - '${VPN_HOST_ADDRESS:-0.0.0.0}:443:443' volumes: - /var/run/docker.sock:/var/run/docker.sock - /data/traefik/acme.json:/acme.json diff --git a/infrastructure/docker-compose.qa-deploy.yml b/infrastructure/docker-compose.qa-deploy.yml index fa1f084f9..cbb45e746 100644 --- a/infrastructure/docker-compose.qa-deploy.yml +++ b/infrastructure/docker-compose.qa-deploy.yml @@ -95,5 +95,46 @@ services: - QA_ENV=true - NODE_ENV=production + wg-easy: + image: weejewel/wg-easy:7 + environment: + - WG_HOST=vpn.{{hostname}} + - PASSWORD=${WIREGUARD_ADMIN_PASSWORD} + - WG_DEFAULT_ADDRESS=10.13.13.x + - WG_ALLOWED_IPS=0.0.0.0/0 + - WG_PORT=51822 + - WG_POST_UP=iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE + - WG_POST_DOWN=iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth+ -j MASQUERADE + volumes: + - /data/wireguard:/etc/wireguard + ports: + - '51822:51820/udp' + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv4.ip_forward=1 + deploy: + labels: + - 'traefik.enable=true' + - 'traefik.http.routers.vpn.rule=Host(`vpn.{{hostname}}`)' + - 'traefik.http.services.vpn.loadbalancer.server.port=51821' + - 'traefik.http.routers.vpn.tls=true' + - 'traefik.http.routers.vpn.entrypoints=web,websecure' + - 'traefik.docker.network=opencrvs_vpn' + - 'traefik.http.middlewares.vpn.headers.customresponseheaders.Pragma=no-cache' + - 'traefik.http.middlewares.vpn.headers.customresponseheaders.Cache-control=no-store' + - 'traefik.http.middlewares.vpn.headers.customresponseheaders.X-Robots-Tag=none' + - 'traefik.http.middlewares.vpn.headers.stsseconds=31536000' + - 'traefik.http.middlewares.vpn.headers.stsincludesubdomains=true' + - 'traefik.http.middlewares.vpn.headers.stspreload=true' + restart: unless-stopped + networks: + - vpn + networks: overlay_net: {} + vpn: + driver: overlay + attachable: false diff --git a/infrastructure/server-setup/production.ini b/infrastructure/server-setup/production.ini index b8cb40e35..408b3d75a 100644 --- a/infrastructure/server-setup/production.ini +++ b/infrastructure/server-setup/production.ini @@ -7,6 +7,12 @@ ; ; Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. +[all:vars] +; This configuration variable blocks all access to the server, including SSH, except from the IP addresses specified below. +; This should always be set when configuring a production server if there is no other firewall in front of the server. +; SSH and other services should never be exposed to the public internet. +only_allow_access_from_addresses=165.22.110.53 + [docker-manager-first] farajaland-prod ansible_host="165.22.205.62" data_label=data1 diff --git a/infrastructure/server-setup/tasks/ufw.yml b/infrastructure/server-setup/tasks/ufw.yml index f91f5b962..67b644523 100644 --- a/infrastructure/server-setup/tasks/ufw.yml +++ b/infrastructure/server-setup/tasks/ufw.yml @@ -3,10 +3,27 @@ name: ufw state: present -- name: 'Allow OpenSSH through UFW' +- name: Allow OpenSSH for IPv4 from specific addresses + ufw: + rule: allow + port: 22 + proto: tcp + src: '{{ item }}' + loop: '{{ only_allow_access_from_addresses }}' + when: only_allow_access_from_addresses is defined and only_allow_access_from_addresses | length > 0 + +- name: Remove general OpenSSH allow rule + ufw: + rule: allow + name: OpenSSH + delete: yes + when: only_allow_access_from_addresses is defined and only_allow_access_from_addresses | length > 0 + +- name: Allow OpenSSH through UFW universally ufw: rule: allow name: OpenSSH + when: only_allow_access_from_addresses is undefined or only_allow_access_from_addresses | length == 0 # Docker swarm ports - Note: all published docker container port will override UFW rules! - name: 'Allow secure docker client communication' diff --git a/package.json b/package.json index 1adcb4c01..e93840125 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "jsonwebtoken": "^9.0.0", "libsodium-wrappers": "^0.7.13", "lint-staged": "^7.1.0", + "minimist": "^1.2.8", "niceware": "^2.0.2", "nodemon": "^2.0.22", "prettier": "^2.8.8", diff --git a/yarn.lock b/yarn.lock index 3f62bf7b9..2a4134e1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7892,6 +7892,11 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.2, minimist@^1.2.5, minimist@^1. resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== +minimist@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"