diff --git a/services/api-db/Dockerfile b/services/api-db/Dockerfile index c8f5bd2f1e..d95f048580 100644 --- a/services/api-db/Dockerfile +++ b/services/api-db/Dockerfile @@ -1,6 +1,10 @@ ARG IMAGE_REPO FROM ${IMAGE_REPO:-lagoon}/mariadb +USER root +RUN apk add --no-cache openssh-keygen +USER mysql + ENV MARIADB_DATABASE=infrastructure \ MARIADB_USER=api \ MARIADB_PASSWORD=api \ diff --git a/services/api-db/docker-entrypoint-initdb.d/00-tables.sql b/services/api-db/docker-entrypoint-initdb.d/00-tables.sql index 8ad9133bed..42591ee1b5 100644 --- a/services/api-db/docker-entrypoint-initdb.d/00-tables.sql +++ b/services/api-db/docker-entrypoint-initdb.d/00-tables.sql @@ -3,11 +3,12 @@ USE infrastructure; -- Tables CREATE TABLE IF NOT EXISTS ssh_key ( - id int NOT NULL auto_increment PRIMARY KEY, - name varchar(100) NOT NULL, - key_value varchar(5000) NOT NULL, - key_type ENUM('ssh-rsa', 'ssh-ed25519') NOT NULL DEFAULT 'ssh-rsa', - created timestamp DEFAULT CURRENT_TIMESTAMP + id int NOT NULL auto_increment PRIMARY KEY, + name varchar(100) NOT NULL, + key_value varchar(5000) NOT NULL, + key_type ENUM('ssh-rsa', 'ssh-ed25519') NOT NULL DEFAULT 'ssh-rsa', + key_fingerprint char(51) NULL UNIQUE, + created timestamp DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS user ( diff --git a/services/api-db/docker-entrypoint-initdb.d/01-migrations.sql b/services/api-db/docker-entrypoint-initdb.d/01-migrations.sql index c5438ec42e..b454806a48 100644 --- a/services/api-db/docker-entrypoint-initdb.d/01-migrations.sql +++ b/services/api-db/docker-entrypoint-initdb.d/01-migrations.sql @@ -588,6 +588,24 @@ CREATE OR REPLACE PROCEDURE END; $$ +CREATE OR REPLACE PROCEDURE + add_key_fingerprint_to_ssh_key() + + BEGIN + IF NOT EXISTS( + SELECT NULL + FROM INFORMATION_SCHEMA.COLUMNS + WHERE + table_name = 'ssh_key' + AND table_schema = 'infrastructure' + AND column_name = 'key_fingerprint' + ) THEN + ALTER TABLE `ssh_key` + ADD `key_fingerprint` char(51) NULL UNIQUE; + END IF; + END; +$$ + DELIMITER ; CALL add_production_environment_to_project(); @@ -618,6 +636,7 @@ CALL add_default_value_to_task_status(); CALL add_scope_to_env_vars(); CALL add_deleted_to_environment_backup(); CALL convert_task_command_to_text(); +CALL add_key_fingerprint_to_ssh_key(); -- Drop legacy SSH key procedures DROP PROCEDURE IF EXISTS CreateProjectSshKey; diff --git a/services/api-db/docker-entrypoint-initdb.d/04-generate-ssh-key-fingerprints.sh b/services/api-db/docker-entrypoint-initdb.d/04-generate-ssh-key-fingerprints.sh new file mode 100755 index 0000000000..924f860699 --- /dev/null +++ b/services/api-db/docker-entrypoint-initdb.d/04-generate-ssh-key-fingerprints.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +set -eu -o pipefail + +# disable globbing +set -f; +# set field separator to NL (only) +IFS=$'\n'; + +DUPLICATE_SSHKEY_RECORDS=( $(mysql infrastructure --batch -sse "SELECT count(*) count, key_value FROM ssh_key GROUP BY key_value HAVING count > 1") ); + +if [ ${#DUPLICATE_SSHKEY_RECORDS[@]} -ne 0 ]; then + echo "====== FOUND DUPLICATE SSH KEYS IN LAGOON API DATABASE!" + for DUPLICATE_SSHKEY_RECORD in "${DUPLICATE_SSHKEY_RECORDS[@]}"; + do + echo "" + echo $(awk '{print $2}' <<< "$DUPLICATE_SSHKEY_RECORD"); + done; + echo "" + echo "====== PLEASE REMOVE DUPLICATED SSH KEYS AND RUN INITIALIZATION OF DB AGAIN" + #exit 1 +fi + +echo "=== Starting SSH KEY Fingerprint generation" + +# get all ssh keys which have no fingerprint yet from api-db into a bash array +SSHKEY_RECORDS=( $(mysql infrastructure --batch -sse "SELECT id, key_type, key_value FROM ssh_key WHERE key_fingerprint is NULL") ); + +for SSHKEY_RECORD in "${SSHKEY_RECORDS[@]}"; +do + RECORD_ID=$(awk '{print $1}' <<< "$SSHKEY_RECORD"); + SSHKEY=$(awk '{print $2, $3}' <<< "$SSHKEY_RECORD"); + FINGERPRINT=$(ssh-keygen -lE sha256 -f - <<< "$SSHKEY" | awk '{print $2}'); + echo "Adding SSH Key Fingerprint for SSH KEY '$RECORD_ID': $FINGERPRINT" + mysql infrastructure -e "UPDATE ssh_key SET key_fingerprint = '$FINGERPRINT' WHERE id = $RECORD_ID"; +done; + +echo "=== Finished SSH KEY Fingerprint generation" diff --git a/services/api-db/rerun_initdb.sh b/services/api-db/rerun_initdb.sh index 2690dc95d3..b482adfedb 100755 --- a/services/api-db/rerun_initdb.sh +++ b/services/api-db/rerun_initdb.sh @@ -1,5 +1,10 @@ -#!/bin/sh +#!/bin/bash -INITDB_DIR="/docker-entrypoint-initdb.d" - -for sql_file in `ls $INITDB_DIR`; do mysql --verbose < "$INITDB_DIR/$sql_file" ; done +for f in `ls /docker-entrypoint-initdb.d/*`; do + case "$f" in + *.sh) echo "$0: running $f"; . "$f" ;; + *.sql) echo "$0: running $f"; cat $f| tee | mysql --verbose; echo ;; + *) echo "$0: ignoring $f" ;; + esac +echo +done \ No newline at end of file diff --git a/services/api/src/resources/sshKey/index.js b/services/api/src/resources/sshKey/index.js index 6ba2948ce4..205b69eeef 100644 --- a/services/api/src/resources/sshKey/index.js +++ b/services/api/src/resources/sshKey/index.js @@ -14,6 +14,12 @@ const validateSshKey = (key /* : string */) /* : boolean */ => { } }; +const getSshKeyFingerprint = (key /* : string */) /* : string */ => { + const parsed = sshpk.parseKey(key, 'ssh'); + return parsed.fingerprint('sha256', 'ssh').toString(); +}; + module.exports = { validateSshKey, + getSshKeyFingerprint, }; diff --git a/services/api/src/resources/sshKey/resolvers.js b/services/api/src/resources/sshKey/resolvers.js index acddae8d60..6f5570abf4 100644 --- a/services/api/src/resources/sshKey/resolvers.js +++ b/services/api/src/resources/sshKey/resolvers.js @@ -3,7 +3,7 @@ const R = require('ramda'); const sqlClient = require('../../clients/sqlClient'); const { isPatchEmpty, prepare, query } = require('../../util/db'); -const { validateSshKey } = require('.'); +const { validateSshKey, getSshKeyFingerprint } = require('.'); const Sql = require('./sql'); /* :: @@ -66,8 +66,9 @@ const addSshKey = async ( { credentials: { role, userId: credentialsUserId } }, ) => { const keyType = sshKeyTypeToString(unformattedKeyType); + const keyFormatted = formatSshKey({ keyType, keyValue }); - if (!validateSshKey(formatSshKey({ keyType, keyValue }))) { + if (!validateSshKey(keyFormatted)) { throw new Error('Invalid SSH key format! Please verify keyType + keyValue'); } @@ -86,6 +87,7 @@ const addSshKey = async ( name, keyValue, keyType, + keyFingerprint: getSshKeyFingerprint(keyFormatted), }), ); await query(sqlClient, Sql.addSshKeyToUser({ sshKeyId: insertId, userId })); @@ -125,16 +127,20 @@ const updateSshKey = async ( throw new Error('Input patch requires at least 1 attribute'); } - if ( - (keyType || keyValue) && - !validateSshKey(formatSshKey({ keyType, keyValue })) - ) { - throw new Error('Invalid SSH key format! Please verify keyType + keyValue'); + let keyFingerprint = null; + if ((keyType || keyValue)) { + const keyFormatted = formatSshKey({ keyType, keyValue }); + + if (!validateSshKey(keyFormatted)) { + throw new Error('Invalid SSH key format! Please verify keyType + keyValue'); + } + + keyFingerprint = getSshKeyFingerprint(keyFormatted); } await query( sqlClient, - Sql.updateSshKey({ id, patch: { name, keyType, keyValue } }), + Sql.updateSshKey({ id, patch: { name, keyType, keyValue, keyFingerprint } }), ); const rows = await query(sqlClient, Sql.selectSshKey(id)); diff --git a/services/api/src/resources/sshKey/sql.js b/services/api/src/resources/sshKey/sql.js index ce5aa717eb..21269b55e7 100644 --- a/services/api/src/resources/sshKey/sql.js +++ b/services/api/src/resources/sshKey/sql.js @@ -58,11 +58,13 @@ const Sql /* : SqlObj */ = { name, keyValue, keyType, + keyFingerprint, } /* : { id: number, name: string, keyValue: string, keyType: string, + keyFingerprint: string, } */, ) => knex('ssh_key') @@ -71,6 +73,7 @@ const Sql /* : SqlObj */ = { name, key_value: keyValue, key_type: keyType, + key_fingerprint: keyFingerprint, }) .toString(), addSshKeyToUser: ( diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index 2a3121cf48..9ff4bff9f3 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -78,6 +78,7 @@ const typeDefs = gql` name: String keyValue: String keyType: String + keyFingerprint: String created: String }