diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f15266c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +node_modules +npm-debug.log +yarn-debug.log +yarn-error.log +.env +*.md +.DS_Store +.vscode +.idea +*.log +coverage/ +test/ +__tests__/ +*.test.* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7b52c9a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,94 @@ +# syntax=docker/dockerfile:1.4 + +# Build stage +FROM node:22.1.0-alpine@sha256:487dc5d5122d578e13f2231aa4ac0f63068becd921099c4c677c850df93bede8 AS builder + +# Set build-time variables for reproducibility +ARG NODE_ENV=development +ARG BUILD_VERSION=dev +ARG BUILD_DATE=unknown +ARG VCS_REF=unknown +ARG PORT=3081 + +# Set environment variables +ENV NODE_ENV=${NODE_ENV} \ + NODE_VERSION=22.1.0 + +# Set build-time labels +LABEL org.opencontainers.image.created=${BUILD_DATE} \ + org.opencontainers.image.version=${BUILD_VERSION} \ + org.opencontainers.image.revision=${VCS_REF} + +# Set consistent timezone and locale +ENV TZ=UTC \ + LANG=C.UTF-8 + +# Create app directory +WORKDIR /usr/src/app + +# Install build dependencies +RUN --mount=type=cache,target=/var/cache/apk \ + apk add --no-cache \ + python3 \ + make \ + g++ \ + gcc \ + linux-headers + +# Copy dependency files +COPY package.json yarn.lock ./ + +# Install dependencies with cache mount +RUN --mount=type=cache,target=/usr/src/app/.yarn-cache \ + yarn install --frozen-lockfile --production=false --cache-folder /usr/src/app/.yarn-cache && \ + yarn cache clean && \ + rm -rf /usr/src/app/.yarn-cache/* + +# Copy source code +COPY . . + +# Build TypeScript code with deterministic output +RUN yarn build + +FROM node:22.1.0-alpine@sha256:487dc5d5122d578e13f2231aa4ac0f63068becd921099c4c677c850df93bede8 AS production + +# Declare build arguments in production stage +ARG PORT=3081 +ARG NODE_ENV=development + +# Set build-time labels +LABEL org.opencontainers.image.created=${BUILD_DATE} \ + org.opencontainers.image.version=${BUILD_VERSION} \ + org.opencontainers.image.revision=${VCS_REF} + +# Set runtime environment +ENV NODE_ENV=${NODE_ENV} \ + PORT=${PORT} \ + TZ=UTC \ + LANG=C.UTF-8 + +WORKDIR /usr/src/app + +# Create non-root user, certificate directory and logs directory +RUN addgroup -S bitgo && \ + adduser -S bitgo -G bitgo && \ + mkdir -p /app/certs && \ + mkdir -p /usr/src/app/logs && \ + chown -R bitgo:bitgo /app/certs && \ + chown -R bitgo:bitgo /usr/src/app && \ + chmod 750 /app/certs && \ + chmod 750 /usr/src/app/logs + +# Copy only necessary files from builder +COPY --from=builder --chown=bitgo:bitgo /usr/src/app/dist ./dist +COPY --from=builder --chown=bitgo:bitgo /usr/src/app/node_modules ./node_modules +COPY --from=builder --chown=bitgo:bitgo /usr/src/app/bin ./bin +COPY --from=builder --chown=bitgo:bitgo /usr/src/app/package.json . + +USER bitgo + +# Expose port from build arg +EXPOSE ${PORT} + +# Start the application using the binary +CMD ["./bin/enclaved-bitgo-express"] \ No newline at end of file diff --git a/README.md b/README.md index 6c00302..dca8a9c 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,74 @@ ALLOW_SELF_SIGNED=false \ yarn start ``` +## Container Deployment with Podman + +First, build the container image: + +```bash +# For Master Express (default port 3081) +yarn container:build + +# For Enclaved Express (port 3080) +yarn container:build --build-arg PORT=3080 +``` + +For local development, you'll need to run both the Enclaved Express and Master Express containers: + +```bash +# Start Enclaved Express container +podman run -d \ + -p 3080:3080 \ + -v $(pwd)/certs:/app/certs:Z \ + -e APP_MODE=enclaved \ + -e BIND=0.0.0.0 \ + -e TLS_MODE=mtls \ + -e TLS_KEY_PATH=/app/certs/enclaved-express-key.pem \ + -e TLS_CERT_PATH=/app/certs/enclaved-express-cert.pem \ + -e KMS_URL=host.containers.internal:3000 \ + -e NODE_ENV=development \ + -e ALLOW_SELF_SIGNED=true \ + bitgo-onprem-express + +# View logs +podman logs -f + +# Test the endpoint (note: using https) +curl -k -X POST https://localhost:3080/ping + +# Start Master Express container +podman run -d \ + -p 3081:3081 \ + -v $(pwd)/certs:/app/certs:Z \ + -e APP_MODE=master-express \ + -e BIND=0.0.0.0 \ + -e TLS_MODE=mtls \ + -e TLS_KEY_PATH=/app/certs/test-ssl-key.pem \ + -e TLS_CERT_PATH=/app/certs/test-ssl-cert.pem \ + -e ENCLAVED_EXPRESS_URL=https://host.containers.internal:3080 \ + -e ENCLAVED_EXPRESS_CERT=/app/certs/enclaved-express-cert.pem \ + -e ALLOW_SELF_SIGNED=true \ + bitgo-onprem-express + +# View logs +podman logs -f + +# Test the endpoints (note: using https and mTLS) +# For Enclaved Express +curl -k --cert certs/test-ssl-cert.pem --key certs/enclaved-express-key.pem -X POST https://localhost:3080/ping + +# For Master Express +curl -k --cert certs/test-ssl-cert.pem --key certs/test-ssl-key.pem -X POST https://localhost:3081/ping + +# Test the connection +curl -k -X POST https://localhost:3081/ping/enclavedExpress +``` + +Notes: +- `host.containers.internal` is a special DNS name that resolves to the host machine from inside containers +- The `:Z` option in volume mounts is specific to SELinux-enabled systems and ensures proper volume labeling +- The logs directory will be created with appropriate permissions if it doesn't exist + ## API Endpoints ### Enclaved Express (Port 3080) diff --git a/certs/enclaved-express-cert.pem b/certs/enclaved-express-cert.pem new file mode 100644 index 0000000..6076da4 --- /dev/null +++ b/certs/enclaved-express-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUbE+vqSu9IgPoLJncJqX5aiXh2GkwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDUzMDIwMDgxNVoXDTI2MDUz +MDIwMDgxNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAsK1g8ts/QRdEHVnmiZSijvtKl08yf13JWY0yksJW0O6x +mrt2uotvONxMNKhGtS+hPjcJ2OC7fCyift8oaCDs7PfIXjVNcN2zRKPci8ihNWvQ +XrYGLTvL9EVHpH7CdlJU43BTaeFusH+k/qv2pW5WQnz13ULdq7yvnDFvJAeahm9X +ptvr9RX9f8Aki0Y82Zi04PCiaHdqBPPl1OfHi+brf4xl7pQUq7Pub94/IDywe+QK +lGFPQ0exSVm5X/7hWv/AxqEFa/Bqb6Uw0qatVqhrgLEHlLUYVXs9NDNXm+865+aT +kvW2dnBpTVRZjnXO+N+BwSj+PfI28RqMXsmIhraN4QIDAQABo1MwUTAdBgNVHQ4E +FgQUnsZxpWiuxqDq/1kV12rMos4NN/cwHwYDVR0jBBgwFoAUnsZxpWiuxqDq/1kV +12rMos4NN/cwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAsAX4 +CCEsIVrKQKJKluEDqOFiuKg0SSe4xVqlSW9vy9z3UYLfOpw14EtB6Lzbtgw7z47w +AnZZS99Zzn3tbYd06/X+b3jThF5TU1gqBcYDCC9HCd9xpmQEC7Ss1Xa88ubjuh+U +E/7xN5xRt85S07VihJWscfY7JCUAELBo3gDCZLfgHjw8xMfPRceE36rkc5B2p60b +WEmmOBWjrSboMOfocasBTUVUMDvGgmxEGEmKgTYshr5lWKcIteisbZi7+OZlkflp +PUZNu5DUyQyjftr2EShndaceZrjgXt6ezoyQBVgPRA+N+NAJn+uBr3B3nZZb/mft +n3XsbtsAAoU49kEVOg== +-----END CERTIFICATE----- diff --git a/certs/enclaved-express-key.pem b/certs/enclaved-express-key.pem new file mode 100644 index 0000000..9d8540f --- /dev/null +++ b/certs/enclaved-express-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwrWDy2z9BF0Qd +WeaJlKKO+0qXTzJ/XclZjTKSwlbQ7rGau3a6i2843Ew0qEa1L6E+NwnY4Lt8LKJ+ +3yhoIOzs98heNU1w3bNEo9yLyKE1a9BetgYtO8v0RUekfsJ2UlTjcFNp4W6wf6T+ +q/alblZCfPXdQt2rvK+cMW8kB5qGb1em2+v1Ff1/wCSLRjzZmLTg8KJod2oE8+XU +58eL5ut/jGXulBSrs+5v3j8gPLB75AqUYU9DR7FJWblf/uFa/8DGoQVr8GpvpTDS +pq1WqGuAsQeUtRhVez00M1eb7zrn5pOS9bZ2cGlNVFmOdc7434HBKP498jbxGoxe +yYiGto3hAgMBAAECggEACrbTnJwBJQBf3WvN7Y/5n7Qg3ODXo3Ow5Iu0gm6N9z5S +akYYuCKr4bCHtTXIkUT3K/UIgCzOEdoJwf85zb7EFMbIpuCSoVfrKYF1EXZe7a9r +w81EE0rUs9aJDAeyi/Gy5iwHUvIcf/rtqugLcr194QBU+fsLwwC+oY6POonKLCwg +gXMxRJKx+tvp86x6s7FU8+vi40L/mGbCC1Bl1YqraVp8nT17ICivlcHVZESxCcKN +tCAY+xKK+zH+s5sHAQvM4OlGEvCeT1VISlw/VqkODxcGzMGUc+mbnGtWvDcYm9Pa +54F29QapkdUwBVnucpTICenaMrLr19H9l6Zgvfvx1QKBgQDnHHeGnSD1bpZUhIUt +2vIQpj7o26zsx472h9PmqIZwODpcYSfw8MknymXVnL78gdHqVDL0mgj59zemUpC7 +CR9RJAlV7/3TghUPdDFQ/SGj9+xGG/L/HNyy6bQeLIZiGOlURcdFqAtKCIk+51oK +eTDCOuy3Ijrq6F6FbYnkXHmwcwKBgQDDtDdCo9EjnyiZ1qGOx5jLRNbGVlN77QFS +tSmegODAwfLQpm4c4fE2WnzeWlNXnzs8GRLSRASXIYunjQdvgpX1KTpffZPoSP3k +tvL8cbh1zk7X8cmvkrVJjcpn/ecWPgeHGV6MjuhxqhaVoEMjFsKRqQaSQ7r/gZsm +Vba82tuXWwKBgHFlSlBGcJF7/U7i5uWk8/ivWVav0p0rHT5hTttx/OS68ge5s/tI +aaqYaHbzPdJvcCvlvEq/+X+MiUWWZWUgCLmrUNlVs9k/jk3S2Q+/4+2sC8YqmIQM +CU3P1YyolBc12eZ7hlbrKP7eSVkP8uIIrJ/ggZ0psnboJNia8nmV1i95AoGAPNWE +Z/6sQDp1UHzbc5qv8F/Rs42aHeeqhZ8y9MZzFvgzFpDloazKYm72adgCGDazHxdc +NmhWVPRkiQzZxtv86VyLfKt4krg91B7aoYZoJJahA5dxblZYbCjbRkAy2UMm6+QC +9AZoUwzgQFq1A+9LRCQamtTbCBmttNjoGQSfRgkCgYEAi03ZXB+B0/4C2HRUz/GQ +6moLgB7FzC4MLY2KUDeiP+3zBPnfbGQM0OgPYu7OOWPC6lebS4C6DuPCTOSW1z8u +f4FeVSKGrofPx+DmEvUMsUQ5TvjRPwNL40PVlrdxytZi6nV01GveScPfljvQUd2r +DRaZg+YgV9Yl6wi8y2G5RD0= +-----END PRIVATE KEY----- diff --git a/certs/test-ssl-cert.pem b/certs/test-ssl-cert.pem new file mode 100644 index 0000000..773b5a0 --- /dev/null +++ b/certs/test-ssl-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUYN0EUBLwq7uoLwDuTx7gDW0HS2UwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDUxNDIxMTUzMVoXDTI2MDUx +NDIxMTUzMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAmxdCF7xCJcE6yyG+wrdhHMwRiQxbhDLW/hiKQcO/nn7z +QMM9LAV04+WG5LCmm0ygocbfXXhfWn5D6SSSESKTb8dbSj6AJbLSmYjA5vDSzh9R +rO9GzNTDCDne+epo+rN4BOjGh7K83naLei/bEfmklMp7x+TyoBC8Ps/3Eq4HOJfd +UzgV2L3oC/4dCbAnkgK2zanL8KEaH6aM0HytIaqMFYLBs2t8s7HHHcSEadHfjlJu +GwTmTS0nVhJBWYJvF6Pv/SwLFuSo93TJybaMMUSF3oJK35NEYXg6EtibJLUC9RvX +FVLytRA5z+x7FBnGBdi4ctMseecokV4u15ePCB3MXwIDAQABo1MwUTAdBgNVHQ4E +FgQUxHrQZFFBfTfuXeOOoXmHZQ8E6rswHwYDVR0jBBgwFoAUxHrQZFFBfTfuXeOO +oXmHZQ8E6rswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAj0hy +bwWh2B26cR0ADhuDC0MtGPBH0BrHypJEZ96nUPTY4oxRrjZusgdq60ooqsNyIW6k +cZHkajC/O0V6hnV4yMhLE8ZwA+31iyQakonpT5N1OON4Ddv9Bfvx8xOn75x/+RP+ +GlAa31XxivryIi/5y7MEI0PwU34T/6bbMWdBaFRQtbuIXJ/90AZ0fBwIV0vJWjaO +1DriZAJe7hl63ZUw6CsfutpoyKkanF5GQB2CpolR3t1oeHwuDbZ550p1g2XFB6UI +9W+zlggQFeAnthzMoi3erO4sQ3j2b15QLZbk1HXHZcn3+89QcvdUcpG0u51bZdFW +SJ+bnT3TZ9H+szoa1w== +-----END CERTIFICATE----- diff --git a/certs/test-ssl-key.pem b/certs/test-ssl-key.pem new file mode 100644 index 0000000..cf4f9ef --- /dev/null +++ b/certs/test-ssl-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCbF0IXvEIlwTrL +Ib7Ct2EczBGJDFuEMtb+GIpBw7+efvNAwz0sBXTj5YbksKabTKChxt9deF9afkPp +JJIRIpNvx1tKPoAlstKZiMDm8NLOH1Gs70bM1MMIOd756mj6s3gE6MaHsrzedot6 +L9sR+aSUynvH5PKgELw+z/cSrgc4l91TOBXYvegL/h0JsCeSArbNqcvwoRofpozQ +fK0hqowVgsGza3yzsccdxIRp0d+OUm4bBOZNLSdWEkFZgm8Xo+/9LAsW5Kj3dMnJ +towxRIXegkrfk0RheDoS2JsktQL1G9cVUvK1EDnP7HsUGcYF2Lhy0yx55yiRXi7X +l48IHcxfAgMBAAECggEAAft4dHXgi+ZTX7iiXTobe1MUakxbzcMZ7QzX6jfxTA+o +APeTNuvURFFxDvKUaT8VJ9wzNgOi3F+UHffCB4a0nGTPmDxXm6O/KLKPHKSOso8Y +ln2cdGPHy7l0TdIe3g113EI0FL9GcLrSf5D7Bi4gWhKDJdlETKLKH9dn+2IkD3zE +VM+7pwqYV6XZu2GuZ1om1JE+Hx2D5YLIuRCON0RKpzCLikmM7VErA7sjy7LsCSh9 +ty5n1s9GoNtF+YuOD9WeMWGDonMyJdYWTsmFYoT+LF7W+GHoEwvYm9595QuNnxVx +KQ4P5oKm/EfcSiBhCC8BdCIGch9tQPT7c/syVhHG4QKBgQDTdUVW7rJNnLtHgWF2 +ubjh9b3ZfxPuTEu6ueKON5XXvSgfsMBNgCxwkemefGJ6xIjDu+swud+2H33Tqj23 +GMMTZ1JEzNYINO1m/laSAK+DcL81q4sLLlJTbBhYeE6FBeEO1hAmU2IiYrgU6zbO +eyo4ysXtFJdnHSR9PHjZpt/cMQKBgQC7wmxw2UNPlmwTzl6l7z5DqYxizIfezb9l +pIYrmcD92asxZKPi1soz8PcOn30gGmjtZn/7FkXFHSRsNknUmqJOEbZrvNCcKndz +O+RbKGs8FAKlyog8k3CTToAng4PutsYrAuK/kx84P9FPCgTxdhejRMkfkSebCJQH +fmXRnlRdjwKBgQCm2Drz0rcBIg9q5hz+zp+gOoOnnusc9TozhQPLbvReGzQTfSTe +gamO0LJiiIYzk+rNdfKmqaJoUwS3A/ZaB8G0B6wT+QNPymMfBsNLxBq4PTfBoy68 +jboLdJjpBVP/BZqEWEa51sTxmK7iYo0F8oxn7yaoX7zucUIfRp2cLl0noQKBgB+K +RG8cgAshiJw3IX0cWEhDdfquwvAxfcJURdmTJXE/HFvavREA5cyd4NKLBhjbdt7S +RhNmpWe8Qn8PC430P+l/XjZw7FYfaBtqZyzM+F6KOfuhrwsF9XY5TJvWotX5zAYz +oOVvkGIBjmaJl1T8cnIRvvtXheCsKzmrCO2SfDePAoGAM1ToKwXSeZEm2Kyf/PV9 +74lvBKYP5LP+pSmrcTq9jUbPQy3KmlhBi+kyhVK+2Awmh0J9tzu83C8lWR25L4mc +/Uwjhv2KwmvJKyZ4/5t/oMZ+BsZERSHj39juLNW+UL82M2heM5tv5/MI779SAmzl +VJMN4N1x+L7408dEGu0j2ds= +-----END PRIVATE KEY----- diff --git a/package.json b/package.json index 89f1d98..ee5a88b 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "test:coverage": "nyc mocha --require ts-node/register 'src/**/__tests__/**/*.test.ts'", "lint": "eslint --quiet .", "generate-test-ssl": "openssl req -x509 -newkey rsa:2048 -keyout test-ssl-key.pem -out test-ssl-cert.pem -days 365 -nodes -subj '/CN=localhost'", - "generate:openapi:masterExpress": "npx @api-ts/openapi-generator --name @bitgo/master-bitgo-express ./src/masterBitgoExpress/routers/index.ts > masterBitgoExpress.json" + "generate:openapi:masterExpress": "npx @api-ts/openapi-generator --name @bitgo/master-bitgo-express ./src/masterBitgoExpress/routers/index.ts > masterBitgoExpress.json", + "container:build": "podman build -t bitgo-onprem-express ." }, "dependencies": { "@api-ts/io-ts-http": "^3.2.1", diff --git a/src/masterBitgoExpress/routers/enclavedExpressHealth.ts b/src/masterBitgoExpress/routers/enclavedExpressHealth.ts index c765c42..0df5e51 100644 --- a/src/masterBitgoExpress/routers/enclavedExpressHealth.ts +++ b/src/masterBitgoExpress/routers/enclavedExpressHealth.ts @@ -12,9 +12,8 @@ import { responseHandler } from '../../shared/middleware'; const PingEnclavedResponse: HttpResponse = { 200: t.type({ status: t.string, - // TODO: Move to common definition between enclavedExpress and masterExpress enclavedResponse: t.type({ - message: t.string, + status: t.string, timestamp: t.string, }), }), @@ -79,7 +78,10 @@ export function createEnclavedExpressRouter( return Response.ok({ status: 'Successfully pinged enclaved express', - enclavedResponse: response.body, + enclavedResponse: { + status: response.body.status, + timestamp: response.body.timestamp, + }, }); } catch (error) { logger.error('Failed to ping enclaved express:', { error });