Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Production VPN #809

Merged
merged 3 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 159 additions & 58 deletions infrastructure/create-github-environment.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
euanmillar marked this conversation as resolved.
Show resolved Hide resolved
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!
Expand All @@ -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: ''
euanmillar marked this conversation as resolved.
Show resolved Hide resolved
},
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. *.<your-domain>
Expand All @@ -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,
Expand All @@ -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

Expand All @@ -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,
Expand All @@ -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'
Expand All @@ -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'
}
Expand All @@ -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
}
Expand All @@ -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
}
}

Expand All @@ -185,46 +248,84 @@ 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,
...config.seeding,
...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()
8 changes: 2 additions & 6 deletions infrastructure/docker-compose.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
euanmillar marked this conversation as resolved.
Show resolved Hide resolved
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /data/traefik/acme.json:/acme.json
Expand Down
41 changes: 41 additions & 0 deletions infrastructure/docker-compose.qa-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading