From ff38858c7465c7f3f2d7eaa0e184617632a94100 Mon Sep 17 00:00:00 2001 From: forest Date: Tue, 29 Dec 2020 18:42:38 -0600 Subject: [PATCH 01/41] fixing login email case sensitivity issues --- capsulflask/auth.py | 17 ++++++++++++--- capsulflask/db.py | 2 +- capsulflask/db_model.py | 21 +++++++++++++------ .../schema_migrations/09_down_email_case.sql | 5 +++++ .../schema_migrations/09_up_email_case.sql | 10 +++++++++ 5 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 capsulflask/schema_migrations/09_down_email_case.sql create mode 100644 capsulflask/schema_migrations/09_up_email_case.sql diff --git a/capsulflask/auth.py b/capsulflask/auth.py index 038b692..5815fce 100644 --- a/capsulflask/auth.py +++ b/capsulflask/auth.py @@ -43,17 +43,28 @@ def login(): errors.append("enter a valid email address") if len(errors) == 0: - token = get_model().login(email) + result = get_model().login(email) + token = result[0] + ignoreCaseMatches = result[1] if token is None: errors.append("too many logins. please use one of the existing login links that have been emailed to you") else: link = f"{current_app.config['BASE_URL']}/auth/magic/{token}" + + message = (f"Navigate to {link} to log into Capsul.\n" + "\nIf you didn't request this, ignore this message.") + + if len(ignoreCaseMatches) > 0: + joinedMatches = " or ".join(ignoreCaseMatches) + message = (f"You tried to log in as '{email}', but that account doesn't exist yet. " + "If you would like to create a new account for '{email}', click here {link} " + "If you meant to log in as {joinedMatches}, please return to capsul.org " + "and log in again with the correct (case-sensitive) email address.") current_app.config["FLASK_MAIL_INSTANCE"].send( Message( "Click This Link to Login to Capsul", - body=(f"Navigate to {link} to log into Capsul.\n" - "\nIf you didn't request this, ignore this message."), + body=message, recipients=[email] ) ) diff --git a/capsulflask/db.py b/capsulflask/db.py index 60cc103..9d5fde9 100644 --- a/capsulflask/db.py +++ b/capsulflask/db.py @@ -40,7 +40,7 @@ def init_app(app): hasSchemaVersionTable = False actionWasTaken = False schemaVersion = 0 - desiredSchemaVersion = 8 + desiredSchemaVersion = 9 cursor = connection.cursor() diff --git a/capsulflask/db_model.py b/capsulflask/db_model.py index 9017977..737db1f 100644 --- a/capsulflask/db_model.py +++ b/capsulflask/db_model.py @@ -10,18 +10,26 @@ class DBModel: def login(self, email): self.cursor.execute("SELECT * FROM accounts WHERE email = %s", (email, )) - if len(self.cursor.fetchall()) == 0: - self.cursor.execute("INSERT INTO accounts (email) VALUES (%s)", (email, )) + hasExactMatch = len(self.cursor.fetchall()) + self.cursor.execute("SELECT * FROM accounts WHERE email = %s AND ever_logged_in = TRUE", (email, )) + everLoggedIn = len(self.cursor.fetchall()) + ignoreCaseMatches = [] + if everLoggedIn == 0: + self.cursor.execute("SELECT email FROM accounts WHERE lower_case_email = %s", (email.lower(), )) + ignoreCaseMatches = self.cursor.fetchall() + + if hasExactMatch == 0: + self.cursor.execute("INSERT INTO accounts (email, lower_case_email) VALUES (%s, %s)", (email, email.lower())) self.cursor.execute("SELECT token FROM login_tokens WHERE email = %s", (email, )) if len(self.cursor.fetchall()) > 2: - return None + return (None, ignoreCaseMatches) token = generate() self.cursor.execute("INSERT INTO login_tokens (email, token) VALUES (%s, %s)", (email, token)) self.connection.commit() - return token + return (token, ignoreCaseMatches) def consume_token(self, token): self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s and created > (NOW() - INTERVAL '20 min')", (token, )) @@ -29,6 +37,7 @@ class DBModel: if row: email = row[0] self.cursor.execute("DELETE FROM login_tokens WHERE email = %s", (email, )) + self.cursor.execute("UPDATE accounts SET ever_logged_in = TRUE WHERE email = %s", (email, )) self.connection.commit() return email return None @@ -228,7 +237,7 @@ class DBModel: if row: self.cursor.execute( "DELETE FROM unresolved_btcpay_invoices WHERE id = %s", (id,) ) if not completed: - self.cursor.execute("UPDATE payments SET invalidated = True WHERE email = %s id = %s", (row[0], row[1])) + self.cursor.execute("UPDATE payments SET invalidated = TRUE WHERE email = %s id = %s", (row[0], row[1])) self.connection.commit() @@ -249,7 +258,7 @@ class DBModel: self.connection.commit() def all_accounts(self): - self.cursor.execute("SELECT email, account_balance_warning FROM accounts") + self.cursor.execute("SELECT email, account_balance_warning FROM accounts WHERE ever_logged_in = TRUE ") return list(map(lambda row: dict(email=row[0], account_balance_warning=row[1]), self.cursor.fetchall())) diff --git a/capsulflask/schema_migrations/09_down_email_case.sql b/capsulflask/schema_migrations/09_down_email_case.sql new file mode 100644 index 0000000..050ecdf --- /dev/null +++ b/capsulflask/schema_migrations/09_down_email_case.sql @@ -0,0 +1,5 @@ + +ALTER TABLE accounts DROP COLUMN lower_case_email; +ALTER TABLE accounts DROP COLUMN ever_logged_in; + +UPDATE schemaversion SET version = 8; \ No newline at end of file diff --git a/capsulflask/schema_migrations/09_up_email_case.sql b/capsulflask/schema_migrations/09_up_email_case.sql new file mode 100644 index 0000000..01b3e71 --- /dev/null +++ b/capsulflask/schema_migrations/09_up_email_case.sql @@ -0,0 +1,10 @@ +ALTER TABLE accounts +ADD COLUMN lower_case_email TEXT NULL; + +ALTER TABLE accounts +ADD COLUMN ever_logged_in BOOLEAN NOT NULL DEFAULT FALSE; + +UPDATE accounts set lower_case_email = LOWER(accounts.email); +UPDATE accounts set ever_logged_in = TRUE; + +UPDATE schemaversion SET version = 9; From eba3bd6a5a58194ea562f102b52d4f153469aed7 Mon Sep 17 00:00:00 2001 From: forest Date: Tue, 29 Dec 2020 19:03:37 -0600 Subject: [PATCH 02/41] fixing bugs with email ignore case feature --- capsulflask/auth.py | 10 +++++----- capsulflask/db_model.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/capsulflask/auth.py b/capsulflask/auth.py index 5815fce..a98a286 100644 --- a/capsulflask/auth.py +++ b/capsulflask/auth.py @@ -50,15 +50,15 @@ def login(): errors.append("too many logins. please use one of the existing login links that have been emailed to you") else: link = f"{current_app.config['BASE_URL']}/auth/magic/{token}" - + message = (f"Navigate to {link} to log into Capsul.\n" "\nIf you didn't request this, ignore this message.") if len(ignoreCaseMatches) > 0: - joinedMatches = " or ".join(ignoreCaseMatches) - message = (f"You tried to log in as '{email}', but that account doesn't exist yet. " - "If you would like to create a new account for '{email}', click here {link} " - "If you meant to log in as {joinedMatches}, please return to capsul.org " + joinedMatches = " or ".join(map(lambda x: f"'{x}'", ignoreCaseMatches)) + message = (f"You tried to log in as '{email}', but that account doesn't exist yet. \n" + f"If you would like to create a new account for '{email}', click here {link} \n\n" + f"If you meant to log in as {joinedMatches}, please return to https://capsul.org \n" "and log in again with the correct (case-sensitive) email address.") current_app.config["FLASK_MAIL_INSTANCE"].send( diff --git a/capsulflask/db_model.py b/capsulflask/db_model.py index 737db1f..bcf1611 100644 --- a/capsulflask/db_model.py +++ b/capsulflask/db_model.py @@ -15,8 +15,8 @@ class DBModel: everLoggedIn = len(self.cursor.fetchall()) ignoreCaseMatches = [] if everLoggedIn == 0: - self.cursor.execute("SELECT email FROM accounts WHERE lower_case_email = %s", (email.lower(), )) - ignoreCaseMatches = self.cursor.fetchall() + self.cursor.execute("SELECT email FROM accounts WHERE lower_case_email = %s AND email != %s", (email.lower(), email)) + ignoreCaseMatches = list(map(lambda x: x[0], self.cursor.fetchall())) if hasExactMatch == 0: self.cursor.execute("INSERT INTO accounts (email, lower_case_email) VALUES (%s, %s)", (email, email.lower())) From 74621a85dd8cf0568618b530ba870f82278e6c5a Mon Sep 17 00:00:00 2001 From: j3s Date: Thu, 14 Jan 2021 21:31:38 -0600 Subject: [PATCH 03/41] Add guixsystem 1.2.0 to capsul --- capsulflask/db.py | 2 +- capsulflask/schema_migrations/10_down_guixsystem1.2.0.sql | 3 +++ capsulflask/schema_migrations/10_up_guixsystem1.2.0.sql | 4 ++++ capsulflask/templates/changelog.html | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 capsulflask/schema_migrations/10_down_guixsystem1.2.0.sql create mode 100644 capsulflask/schema_migrations/10_up_guixsystem1.2.0.sql diff --git a/capsulflask/db.py b/capsulflask/db.py index 9d5fde9..11d60cb 100644 --- a/capsulflask/db.py +++ b/capsulflask/db.py @@ -40,7 +40,7 @@ def init_app(app): hasSchemaVersionTable = False actionWasTaken = False schemaVersion = 0 - desiredSchemaVersion = 9 + desiredSchemaVersion = 10 cursor = connection.cursor() diff --git a/capsulflask/schema_migrations/10_down_guixsystem1.2.0.sql b/capsulflask/schema_migrations/10_down_guixsystem1.2.0.sql new file mode 100644 index 0000000..04bec59 --- /dev/null +++ b/capsulflask/schema_migrations/10_down_guixsystem1.2.0.sql @@ -0,0 +1,3 @@ +DELETE FROM os_images WHERE id = 'guixsystem120'; + +UPDATE schemaversion SET version = 9; diff --git a/capsulflask/schema_migrations/10_up_guixsystem1.2.0.sql b/capsulflask/schema_migrations/10_up_guixsystem1.2.0.sql new file mode 100644 index 0000000..76be19a --- /dev/null +++ b/capsulflask/schema_migrations/10_up_guixsystem1.2.0.sql @@ -0,0 +1,4 @@ +INSERT INTO os_images (id, template_image_file_name, description, deprecated) +VALUES ('guixsystem120', 'guixsystem/1.2.0/root.img.qcow2', 'Guix System 1.2.0', FALSE); + +UPDATE schemaversion SET version = 10; diff --git a/capsulflask/templates/changelog.html b/capsulflask/templates/changelog.html index 6615055..5b8dea4 100644 --- a/capsulflask/templates/changelog.html +++ b/capsulflask/templates/changelog.html @@ -8,6 +8,7 @@ {% block subcontent %}

    +
  • 2021-01-14: Add Guix System 1.2.0 support, thanks to jgart, ryanprior, and raghavgururajan.
  • 2020-10-29: Add OpenBSD 6.8 and Alpine 3.12 support, remove previous versions.
  • 2020-10-23: Automate VM build system (backend)
  • 2020-10-22: Re-worked FAQ, added more supporting docs
  • From 1c2d5cf605ea6391d913e79c46acdf55809986f6 Mon Sep 17 00:00:00 2001 From: j3s Date: Tue, 19 Jan 2021 19:24:07 -0600 Subject: [PATCH 04/41] Add Alpine 3.13 --- capsulflask/db.py | 2 +- capsulflask/schema_migrations/11_down_alpine_3.13.sql | 5 +++++ capsulflask/schema_migrations/11_up_alpine_3.13.sql | 6 ++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 capsulflask/schema_migrations/11_down_alpine_3.13.sql create mode 100644 capsulflask/schema_migrations/11_up_alpine_3.13.sql diff --git a/capsulflask/db.py b/capsulflask/db.py index 11d60cb..4bf1152 100644 --- a/capsulflask/db.py +++ b/capsulflask/db.py @@ -40,7 +40,7 @@ def init_app(app): hasSchemaVersionTable = False actionWasTaken = False schemaVersion = 0 - desiredSchemaVersion = 10 + desiredSchemaVersion = 11 cursor = connection.cursor() diff --git a/capsulflask/schema_migrations/11_down_alpine_3.13.sql b/capsulflask/schema_migrations/11_down_alpine_3.13.sql new file mode 100644 index 0000000..5b4a883 --- /dev/null +++ b/capsulflask/schema_migrations/11_down_alpine_3.13.sql @@ -0,0 +1,5 @@ +DELETE FROM os_images WHERE id = 'alpine313'; + +UPDATE os_images SET deprecated = FALSE WHERE id = 'alpine312'; + +UPDATE schemaversion SET version = 10; diff --git a/capsulflask/schema_migrations/11_up_alpine_3.13.sql b/capsulflask/schema_migrations/11_up_alpine_3.13.sql new file mode 100644 index 0000000..d46e63a --- /dev/null +++ b/capsulflask/schema_migrations/11_up_alpine_3.13.sql @@ -0,0 +1,6 @@ +INSERT INTO os_images (id, template_image_file_name, description, deprecated) +VALUES ('alpine313', 'alpine/3.13/root.img.qcow2', 'Alpine Linux 3.13', FALSE); + +UPDATE os_images SET deprecated = TRUE WHERE id = 'alpine312'; + +UPDATE schemaversion SET version = 11; From 6a82e6df01717de7707ba5023d99dd216fd56ce7 Mon Sep 17 00:00:00 2001 From: j3s Date: Tue, 19 Jan 2021 19:25:03 -0600 Subject: [PATCH 05/41] bump changelog --- capsulflask/templates/changelog.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/capsulflask/templates/changelog.html b/capsulflask/templates/changelog.html index 5b8dea4..885cdf6 100644 --- a/capsulflask/templates/changelog.html +++ b/capsulflask/templates/changelog.html @@ -8,8 +8,9 @@ {% block subcontent %}

      -
    • 2021-01-14: Add Guix System 1.2.0 support, thanks to jgart, ryanprior, and raghavgururajan.
    • -
    • 2020-10-29: Add OpenBSD 6.8 and Alpine 3.12 support, remove previous versions.
    • +
    • 2021-01-19: Add Alpine Linux 3.13 support
    • +
    • 2021-01-14: Add Guix System 1.2.0 support, thanks to jgart, ryanprior, and raghavgururajan
    • +
    • 2020-10-29: Add OpenBSD 6.8 and Alpine 3.12 support, remove previous versions
    • 2020-10-23: Automate VM build system (backend)
    • 2020-10-22: Re-worked FAQ, added more supporting docs
    • 2020-05-16: Beta version of new Capsul web application
    • From 3f6491f35905319bf8831b72d7f0022920ffceb8 Mon Sep 17 00:00:00 2001 From: forest Date: Wed, 20 Jan 2021 16:43:10 -0600 Subject: [PATCH 06/41] add support for postgres sslmode --- capsulflask/__init__.py | 4 ++++ capsulflask/db.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/capsulflask/__init__.py b/capsulflask/__init__.py index e18e9b1..2e80142 100644 --- a/capsulflask/__init__.py +++ b/capsulflask/__init__.py @@ -27,6 +27,10 @@ app.config.from_mapping( ADMIN_EMAIL_ADDRESSES=os.environ.get("ADMIN_EMAIL_ADDRESSES", default="ops@cyberia.club"), DATABASE_URL=os.environ.get("DATABASE_URL", default="sql://postgres:dev@localhost:5432/postgres"), + + # https://www.postgresql.org/docs/9.1/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS + DATABASE_SSLMODE=os.environ.get("DATABASE_SSLMODE", default="prefer"), + DATABASE_SCHEMA=os.environ.get("DATABASE_SCHEMA", default="public"), MAIL_SERVER=os.environ.get("MAIL_SERVER", default="m1.nullhex.com"), diff --git a/capsulflask/db.py b/capsulflask/db.py index 4bf1152..4329288 100644 --- a/capsulflask/db.py +++ b/capsulflask/db.py @@ -20,7 +20,8 @@ def init_app(app): password = databaseUrl.password, host = databaseUrl.hostname, port = databaseUrl.port, - database = databaseUrl.path[1:] + database = databaseUrl.path[1:], + sslmode = app.config['DATABASE_SSLMODE'] ) schemaMigrations = {} From 274c7a2c7691ebe3e8caf271aca83349752bf3c0 Mon Sep 17 00:00:00 2001 From: forest Date: Fri, 29 Jan 2021 00:13:32 -0600 Subject: [PATCH 07/41] add logging around ssh keys to capsul creation to support a user who is experiencing bugs --- capsulflask/console.py | 10 ++++++++++ capsulflask/db_model.py | 2 ++ capsulflask/virt_model.py | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/capsulflask/console.py b/capsulflask/console.py index 79aa35b..4444b09 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -1,5 +1,6 @@ import re import sys +import json from datetime import datetime, timedelta from flask import Blueprint from flask import flash @@ -127,6 +128,9 @@ def create(): account_balance = get_account_balance(get_vms(), get_payments(), datetime.utcnow()) capacity_avaliable = current_app.config["VIRTUALIZATION_MODEL"].capacity_avaliable(512*1024*1024) errors = list() + + email_to_log = session["account"] + current_app.logger.info(f"create for {email_to_log}: ssh keys from db:\n {json.dumps(ssh_public_keys)}") if request.method == "POST": if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: @@ -151,11 +155,15 @@ def create(): errors.append("something went wrong with ssh keys") else: for i in range(0, posted_keys_count): + to_log_bool = f"ssh_key_{i}" in request.form + current_app.logger.info(f"checking for ssh_key_{i}: {to_log_bool}") if f"ssh_key_{i}" in request.form: posted_name = request.form[f"ssh_key_{i}"] + current_app.logger.info(f"ssh key posted_name: {posted_name}") key = None for x in ssh_public_keys: if x['name'] == posted_name: + current_app.logger.info(f"ssh key posted_name {posted_name} was found") key = x if key: posted_keys.append(key) @@ -172,6 +180,8 @@ def create(): host(s) at capacity. no capsuls can be created at this time. sorry. """) + current_app.logger.info(f"create for {email_to_log}: posted_keys:\n {json.dumps(posted_keys)}") + if len(errors) == 0: id = makeCapsulId() get_model().create_vm( diff --git a/capsulflask/db_model.py b/capsulflask/db_model.py index bcf1611..8216439 100644 --- a/capsulflask/db_model.py +++ b/capsulflask/db_model.py @@ -113,6 +113,8 @@ class DBModel: ) for ssh_public_key in ssh_public_keys: + current_app.logger.info(f"INSERT INTO vm_ssh_public_key (email, vm_id, ssh_public_key_name) VALUES (\"{email}\", \"{id}\", \"{ssh_public_key}\")") + self.cursor.execute(""" INSERT INTO vm_ssh_public_key (email, vm_id, ssh_public_key_name) VALUES (%s, %s, %s) diff --git a/capsulflask/virt_model.py b/capsulflask/virt_model.py index 8ff74cc..f414c0f 100644 --- a/capsulflask/virt_model.py +++ b/capsulflask/virt_model.py @@ -126,6 +126,8 @@ class ShellScriptVirtualization(VirtualizationInterface): ssh_keys_string = "\n".join(ssh_public_keys) + current_app.logger.info(f"create vm virt model ssh_keys_string: {ssh_keys_string}") + completedProcess = run([ join(current_app.root_path, 'shell_scripts/create.sh'), id, @@ -147,6 +149,8 @@ class ShellScriptVirtualization(VirtualizationInterface): ssh_public_keys={ssh_keys_string} """ + current_app.logger.info(f"create vm status: {status} vmSettings: {vmSettings}") + if not status == "success": raise ValueError(f"""failed to create vm for {email} with: {vmSettings} From 7914a2e51842714a1a385a1ead3ebc48018dfd13 Mon Sep 17 00:00:00 2001 From: forest Date: Fri, 29 Jan 2021 00:19:05 -0600 Subject: [PATCH 08/41] fix "datetime is not JSON serializable" err in debug log code --- capsulflask/console.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/capsulflask/console.py b/capsulflask/console.py index 4444b09..2f9832a 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -1,6 +1,5 @@ import re import sys -import json from datetime import datetime, timedelta from flask import Blueprint from flask import flash @@ -129,8 +128,9 @@ def create(): capacity_avaliable = current_app.config["VIRTUALIZATION_MODEL"].capacity_avaliable(512*1024*1024) errors = list() + ssh_keys_from_db_string = "\n".join(list(map(lambda x: f"name: {x.name}**content: {x.content}", ssh_public_keys))) email_to_log = session["account"] - current_app.logger.info(f"create for {email_to_log}: ssh keys from db:\n {json.dumps(ssh_public_keys)}") + current_app.logger.info(f"create for {email_to_log}: ssh keys from db:\n {ssh_keys_from_db_string}") if request.method == "POST": if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: @@ -180,7 +180,8 @@ def create(): host(s) at capacity. no capsuls can be created at this time. sorry. """) - current_app.logger.info(f"create for {email_to_log}: posted_keys:\n {json.dumps(posted_keys)}") + posted_keys_string = "\n".join(list(map(lambda x: f"name: {x.name}**content: {x.content}", posted_keys))) + current_app.logger.info(f"create for {email_to_log}: posted_keys:\n {posted_keys_string}") if len(errors) == 0: id = makeCapsulId() From 40016ecbe6a6ae3f172b4ddfe708c5a76b8506fc Mon Sep 17 00:00:00 2001 From: forest Date: Fri, 29 Jan 2021 00:22:19 -0600 Subject: [PATCH 09/41] fix debug code again :( --- capsulflask/console.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/capsulflask/console.py b/capsulflask/console.py index 2f9832a..5dafc57 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -128,7 +128,7 @@ def create(): capacity_avaliable = current_app.config["VIRTUALIZATION_MODEL"].capacity_avaliable(512*1024*1024) errors = list() - ssh_keys_from_db_string = "\n".join(list(map(lambda x: f"name: {x.name}**content: {x.content}", ssh_public_keys))) + ssh_keys_from_db_string = "\n".join(list(map(lambda x: f"name: {x['name']}**content: {x['content']}", ssh_public_keys))) email_to_log = session["account"] current_app.logger.info(f"create for {email_to_log}: ssh keys from db:\n {ssh_keys_from_db_string}") @@ -180,7 +180,7 @@ def create(): host(s) at capacity. no capsuls can be created at this time. sorry. """) - posted_keys_string = "\n".join(list(map(lambda x: f"name: {x.name}**content: {x.content}", posted_keys))) + posted_keys_string = "\n".join(list(map(lambda x: f"name: {x['name']}**content: {x['content']}", posted_keys))) current_app.logger.info(f"create for {email_to_log}: posted_keys:\n {posted_keys_string}") if len(errors) == 0: From 29008bc9638298151baeb9dca90e8780569c9b88 Mon Sep 17 00:00:00 2001 From: forest Date: Fri, 29 Jan 2021 00:26:21 -0600 Subject: [PATCH 10/41] fix index out of range when parsing output from shell_scripts/get.sh --- capsulflask/virt_model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/capsulflask/virt_model.py b/capsulflask/virt_model.py index f414c0f..e2b413b 100644 --- a/capsulflask/virt_model.py +++ b/capsulflask/virt_model.py @@ -96,6 +96,9 @@ class ShellScriptVirtualization(VirtualizationInterface): completedProcess = run([join(current_app.root_path, 'shell_scripts/get.sh'), id], capture_output=True) self.validate_completed_process(completedProcess) lines = completedProcess.stdout.splitlines() + if len(lines) == 0: + return None + ipaddr = lines[0].decode("utf-8") if not re.match(r"^([0-9]{1,3}\.){3}[0-9]{1,3}$", ipaddr): From 50cea6e0b4b001c28a971b84a240295592390665 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 01:39:48 -0600 Subject: [PATCH 11/41] first crack at adding ssh host key display to the capsul detail page --- capsulflask/cli.py | 3 +- capsulflask/console.py | 42 +++++++++------ capsulflask/db.py | 5 +- capsulflask/db_model.py | 34 +++++++++--- capsulflask/metrics.py | 11 +++- capsulflask/payment.py | 3 +- .../12_down_ssh_host_keys.sql | 7 +++ .../schema_migrations/12_up_ssh_host_keys.sql | 14 +++++ capsulflask/shared.py | 20 +++++++ capsulflask/shell_scripts/ssh-keyscan.sh | 22 ++++++++ capsulflask/static/style.css | 11 +++- capsulflask/templates/capsul-detail.html | 24 ++++++++- capsulflask/virt_model.py | 54 ++++++++++++------- 13 files changed, 199 insertions(+), 51 deletions(-) create mode 100644 capsulflask/schema_migrations/12_down_ssh_host_keys.sql create mode 100644 capsulflask/schema_migrations/12_up_ssh_host_keys.sql create mode 100644 capsulflask/shared.py create mode 100644 capsulflask/shell_scripts/ssh-keyscan.sh diff --git a/capsulflask/cli.py b/capsulflask/cli.py index 1962b01..4baaaa2 100644 --- a/capsulflask/cli.py +++ b/capsulflask/cli.py @@ -11,7 +11,8 @@ from flask import current_app from psycopg2 import ProgrammingError from flask_mail import Message -from capsulflask.db import get_model, my_exec_info_message +from capsulflask.db import get_model +from capsulflask.shared import my_exec_info_message from capsulflask.console import get_account_balance bp = Blueprint('cli', __name__) diff --git a/capsulflask/console.py b/capsulflask/console.py index 5dafc57..c161e7f 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -1,5 +1,6 @@ import re import sys +import json from datetime import datetime, timedelta from flask import Blueprint from flask import flash @@ -15,7 +16,8 @@ from nanoid import generate from capsulflask.metrics import durations as metric_durations from capsulflask.auth import account_required -from capsulflask.db import get_model, my_exec_info_message +from capsulflask.db import get_model +from capsulflask.shared import my_exec_info_message from capsulflask.payment import poll_btcpay_session from capsulflask import cli @@ -25,19 +27,21 @@ def makeCapsulId(): lettersAndNumbers = generate(alphabet="1234567890qwertyuiopasdfghjklzxcvbnm", size=10) return f"capsul-{lettersAndNumbers}" -def double_check_capsul_address(id, ipv4): +def double_check_capsul_address(id, ipv4, get_ssh_host_keys): try: - result = current_app.config["VIRTUALIZATION_MODEL"].get(id) + result = current_app.config["VIRTUALIZATION_MODEL"].get(id, get_ssh_host_keys) if result.ipv4 != ipv4: ipv4 = result.ipv4 get_model().update_vm_ip(email=session["account"], id=id, ipv4=result.ipv4) + if get_ssh_host_keys: + get_model().update_vm_ssh_host_keys(email=session["account"], id=id, ssh_host_keys=result.ssh_host_keys) except: current_app.logger.error(f""" the virtualization model threw an error in double_check_capsul_address of {id}: {my_exec_info_message(sys.exc_info())}""" ) - return ipv4 + return result @bp.route("/") @account_required @@ -53,7 +57,7 @@ def index(): # for now we are going to check the IP according to the virt model # on every request. this could be done by a background job and cached later on... for vm in vms: - vm["ipv4"] = double_check_capsul_address(vm["id"], vm["ipv4"]) + vm["ipv4"] = double_check_capsul_address(vm["id"], vm["ipv4"], False).ipv4 vms = list(map( lambda x: dict( @@ -104,9 +108,17 @@ def detail(id): return render_template("capsul-detail.html", vm=vm, delete=True, deleted=True) else: - vm["ipv4"] = double_check_capsul_address(vm["id"], vm["ipv4"]) + needs_ssh_host_keys = "ssh_host_keys" not in vm or len(vm["ssh_host_keys"]) == 0 + vm_from_virt_model = double_check_capsul_address(vm["id"], vm["ipv4"], needs_ssh_host_keys) + vm["ipv4"] = vm_from_virt_model.ipv4 + if needs_ssh_host_keys: + vm["ssh_host_keys"] = vm_from_virt_model.ssh_host_keys + vm["created"] = vm['created'].strftime("%b %d %Y %H:%M") - vm["ssh_public_keys"] = ", ".join(vm["ssh_public_keys"]) if len(vm["ssh_public_keys"]) > 0 else "" + vm["ssh_authorized_keys"] = ", ".join(vm["ssh_authorized_keys"]) if len(vm["ssh_authorized_keys"]) > 0 else "" + + + current_app.logger.info(f"asd {needs_ssh_host_keys} {json.dumps(vm['ssh_host_keys'])})") return render_template( "capsul-detail.html", @@ -123,12 +135,12 @@ def detail(id): def create(): vm_sizes = get_model().vm_sizes_dict() operating_systems = get_model().operating_systems_dict() - ssh_public_keys = get_model().list_ssh_public_keys_for_account(session["account"]) + public_keys_for_account = get_model().list_ssh_public_keys_for_account(session["account"]) account_balance = get_account_balance(get_vms(), get_payments(), datetime.utcnow()) capacity_avaliable = current_app.config["VIRTUALIZATION_MODEL"].capacity_avaliable(512*1024*1024) errors = list() - ssh_keys_from_db_string = "\n".join(list(map(lambda x: f"name: {x['name']}**content: {x['content']}", ssh_public_keys))) + ssh_keys_from_db_string = "\n".join(list(map(lambda x: f"name: {x['name']}**content: {x['content']}", public_keys_for_account))) email_to_log = session["account"] current_app.logger.info(f"create for {email_to_log}: ssh keys from db:\n {ssh_keys_from_db_string}") @@ -161,7 +173,7 @@ def create(): posted_name = request.form[f"ssh_key_{i}"] current_app.logger.info(f"ssh key posted_name: {posted_name}") key = None - for x in ssh_public_keys: + for x in public_keys_for_account: if x['name'] == posted_name: current_app.logger.info(f"ssh key posted_name {posted_name} was found") key = x @@ -190,7 +202,7 @@ def create(): id=id, size=size, os=os, - ssh_public_keys=list(map(lambda x: x["name"], posted_keys)) + ssh_authorized_keys=list(map(lambda x: x["name"], posted_keys)) ) current_app.config["VIRTUALIZATION_MODEL"].create( email = session["account"], @@ -198,7 +210,7 @@ def create(): template_image_file_name=operating_systems[os]['template_image_file_name'], vcpus=vm_sizes[size]['vcpus'], memory_mb=vm_sizes[size]['memory_mb'], - ssh_public_keys=list(map(lambda x: x["content"], posted_keys)) + ssh_authorized_keys=list(map(lambda x: x["content"], posted_keys)) ) return redirect(f"{url_for('console.index')}?created={id}") @@ -219,9 +231,9 @@ def create(): csrf_token = session["csrf-token"], capacity_avaliable=capacity_avaliable, account_balance=format(account_balance, '.2f'), - ssh_public_keys=ssh_public_keys, - ssh_public_key_count=len(ssh_public_keys), - no_ssh_public_keys=len(ssh_public_keys) == 0, + ssh_public_keys=public_keys_for_account, + ssh_public_key_count=len(public_keys_for_account), + no_ssh_public_keys=len(public_keys_for_account) == 0, operating_systems=operating_systems, cant_afford=len(affordable_vm_sizes) == 0, vm_sizes=affordable_vm_sizes diff --git a/capsulflask/db.py b/capsulflask/db.py index 4329288..b0e9a0d 100644 --- a/capsulflask/db.py +++ b/capsulflask/db.py @@ -9,6 +9,7 @@ from flask import current_app from flask import g from capsulflask.db_model import DBModel +from capsulflask.shared import my_exec_info_message def init_app(app): databaseUrl = urlparse(app.config['DATABASE_URL']) @@ -41,7 +42,7 @@ def init_app(app): hasSchemaVersionTable = False actionWasTaken = False schemaVersion = 0 - desiredSchemaVersion = 11 + desiredSchemaVersion = 12 cursor = connection.cursor() @@ -127,5 +128,3 @@ def close_db(e=None): db_model.cursor.close() current_app.config['PSYCOPG2_CONNECTION_POOL'].putconn(db_model.connection) -def my_exec_info_message(exec_info): - return "{}: {}".format(".".join([exec_info[0].__module__, exec_info[0].__name__]), exec_info[1]) diff --git a/capsulflask/db_model.py b/capsulflask/db_model.py index 8216439..fe362eb 100644 --- a/capsulflask/db_model.py +++ b/capsulflask/db_model.py @@ -104,7 +104,17 @@ class DBModel: self.cursor.execute("UPDATE vms SET last_seen_ipv4 = %s WHERE email = %s AND id = %s", (ipv4, email, id)) self.connection.commit() - def create_vm(self, email, id, size, os, ssh_public_keys): + def update_vm_ssh_host_keys(self, email, id, ssh_host_keys): + for key in ssh_host_keys: + self.cursor.execute(""" + INSERT INTO vm_ssh_host_key (email, vm_id, key_type, content, sha256) + VALUES (%s, %s, %s, %s, %s) + """, + (email, id, key['key_type'], key['content'], key['sha256']) + ) + self.connection.commit() + + def create_vm(self, email, id, size, os, ssh_authorized_keys): self.cursor.execute(""" INSERT INTO vms (email, id, size, os) VALUES (%s, %s, %s, %s) @@ -112,14 +122,14 @@ class DBModel: (email, id, size, os) ) - for ssh_public_key in ssh_public_keys: - current_app.logger.info(f"INSERT INTO vm_ssh_public_key (email, vm_id, ssh_public_key_name) VALUES (\"{email}\", \"{id}\", \"{ssh_public_key}\")") + for ssh_authorized_key in ssh_authorized_keys: + current_app.logger.info(f"INSERT INTO vm_ssh_authorized_key (email, vm_id, ssh_public_key_name) VALUES (\"{email}\", \"{id}\", \"{ssh_authorized_key}\")") self.cursor.execute(""" - INSERT INTO vm_ssh_public_key (email, vm_id, ssh_public_key_name) + INSERT INTO vm_ssh_authorized_key (email, vm_id, ssh_public_key_name) VALUES (%s, %s, %s) """, - (email, id, ssh_public_key) + (email, id, ssh_authorized_key) ) self.connection.commit() @@ -147,11 +157,19 @@ class DBModel: ) self.cursor.execute(""" - SELECT ssh_public_key_name FROM vm_ssh_public_key - WHERE vm_ssh_public_key.email = %s AND vm_ssh_public_key.vm_id = %s""", + SELECT ssh_public_key_name FROM vm_ssh_authorized_key + WHERE vm_ssh_authorized_key.email = %s AND vm_ssh_authorized_key.vm_id = %s""", (email, id) ) - vm["ssh_public_keys"] = list(map( lambda x: x[0], self.cursor.fetchall() )) + vm["ssh_authorized_keys"] = list(map( lambda x: x[0], self.cursor.fetchall() )) + + + self.cursor.execute(""" + SELECT key_type, content, sha256 FROM vm_ssh_host_key + WHERE vm_ssh_host_key.email = %s AND vm_ssh_host_key.vm_id = %s""", + (email, id) + ) + vm["ssh_host_keys"] = list(map( lambda x: dict(key_type=x[0], content=x[1], sha256=x[2]), self.cursor.fetchall() )) return vm diff --git a/capsulflask/metrics.py b/capsulflask/metrics.py index fc33b61..dffaa53 100644 --- a/capsulflask/metrics.py +++ b/capsulflask/metrics.py @@ -5,6 +5,7 @@ from functools import reduce import requests #import json from datetime import datetime +from threading import Lock from io import BytesIO from flask import Blueprint from flask import current_app @@ -15,6 +16,7 @@ from werkzeug.exceptions import abort from capsulflask.db import get_model from capsulflask.auth import account_required +mutex = Lock() bp = Blueprint("metrics", __name__, url_prefix="/metrics") durations = dict( @@ -116,6 +118,7 @@ def get_plot_bytes(metric, capsulid, duration, size): return (502, None) series = prometheus_response.json()["data"]["result"] + if len(series) == 0: now_timestamp = datetime.timestamp(datetime.now()) series = [ @@ -129,13 +132,19 @@ def get_plot_bytes(metric, capsulid, duration, size): series[0]["values"] )) - plot_bytes = draw_plot_png_bytes(time_series_data, scale=scales[metric], size_x=sizes[size][0], size_y=sizes[size][1]) + mutex.acquire() + try: + plot_bytes = draw_plot_png_bytes(time_series_data, scale=scales[metric], size_x=sizes[size][0], size_y=sizes[size][1]) + finally: + mutex.release() return (200, plot_bytes) def draw_plot_png_bytes(data, scale, size_x=3, size_y=1): + #current_app.logger.info(json.dumps(data, indent=4, default=str)) + pyplot.style.use("seaborn-dark") fig, my_plot = pyplot.subplots(figsize=(size_x, size_y)) diff --git a/capsulflask/payment.py b/capsulflask/payment.py index 2f65015..6219de5 100644 --- a/capsulflask/payment.py +++ b/capsulflask/payment.py @@ -20,7 +20,8 @@ from werkzeug.exceptions import abort from capsulflask.auth import account_required -from capsulflask.db import get_model, my_exec_info_message +from capsulflask.db import get_model +from capsulflask.shared import my_exec_info_message bp = Blueprint("payment", __name__, url_prefix="/payment") diff --git a/capsulflask/schema_migrations/12_down_ssh_host_keys.sql b/capsulflask/schema_migrations/12_down_ssh_host_keys.sql new file mode 100644 index 0000000..054222e --- /dev/null +++ b/capsulflask/schema_migrations/12_down_ssh_host_keys.sql @@ -0,0 +1,7 @@ + + +DROP TABLE vm_ssh_host_key; + +ALTER TABLE vm_ssh_authorized_key RENAME TO vm_ssh_public_key; + +UPDATE schemaversion SET version = 11; \ No newline at end of file diff --git a/capsulflask/schema_migrations/12_up_ssh_host_keys.sql b/capsulflask/schema_migrations/12_up_ssh_host_keys.sql new file mode 100644 index 0000000..ae03c1e --- /dev/null +++ b/capsulflask/schema_migrations/12_up_ssh_host_keys.sql @@ -0,0 +1,14 @@ + +CREATE TABLE vm_ssh_host_key ( + email TEXT NOT NULL, + vm_id TEXT NOT NULL, + key_type TEXT NOT NULL, + content TEXT NOT NULL, + sha256 TEXT NOT NULL, + FOREIGN KEY (email, vm_id) REFERENCES vms(email, id) ON DELETE CASCADE, + PRIMARY KEY (email, vm_id, key_type) +); + +ALTER TABLE vm_ssh_public_key RENAME TO vm_ssh_authorized_key; + +UPDATE schemaversion SET version = 12; \ No newline at end of file diff --git a/capsulflask/shared.py b/capsulflask/shared.py new file mode 100644 index 0000000..23d69cf --- /dev/null +++ b/capsulflask/shared.py @@ -0,0 +1,20 @@ + +from typing import List + +# I decided to just use dict everywhere instead because I have to use dict to read it from json +# class SSHHostKey: +# def __init__(self, key_type=None, content=None, sha256=None): +# self.key_type = key_type +# self.content = content +# self.sha256 = sha256 + +class VirtualMachine: + def __init__(self, id, ipv4: str = None, ipv6: str = None, ssh_host_keys: List[dict] = list()): + self.id = id + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.ssh_host_keys = ssh_host_keys + + +def my_exec_info_message(exec_info): + return "{}: {}".format(".".join([exec_info[0].__module__, exec_info[0].__name__]), exec_info[1]) \ No newline at end of file diff --git a/capsulflask/shell_scripts/ssh-keyscan.sh b/capsulflask/shell_scripts/ssh-keyscan.sh new file mode 100644 index 0000000..c730a84 --- /dev/null +++ b/capsulflask/shell_scripts/ssh-keyscan.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +ip_address="$1" + +if echo "$ip_address" | grep -vqE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then + echo "ip_address $ip_address must match "'"^([0-9]{1,3}\.){3}[0-9]{1,3}$"' + exit 1 +fi + +printf '[' +DELIMITER="\n" +ssh-keyscan "$ip_address" 2>/dev/null | while read -r line; do + if echo "$line" | grep -qE "^$ip_address"' +(ssh|ecdsa)-[0-9A-Za-z+/_=@. -]+$'; then + KEY_CONTENT="$(echo "$line" | awk '{ print $2 " " $3 }')" + FINGERPRINT_OUTPUT="$(echo "$KEY_CONTENT" | ssh-keygen -l -E sha256 -f - | sed -E 's/^[0-9]+ SHA256:([0-9A-Za-z+/-]+) .+ \(([A-Z0-9]+)\)$/\1 \2/g')" + SHA256_HASH="$(echo "$FINGERPRINT_OUTPUT" | awk '{ print $1 }')" + KEY_TYPE="$(echo "$FINGERPRINT_OUTPUT" | awk '{ print $2 }')" + printf '%s{"key_type":"%s", "content":"%s", "sha256":"%s"}' "$DELIMITER" "$KEY_TYPE" "$KEY_CONTENT" "$SHA256_HASH" + DELIMITER=",\n" + fi +done +printf '\n]\n' diff --git a/capsulflask/static/style.css b/capsulflask/static/style.css index 80ffae3..4ad29f6 100644 --- a/capsulflask/static/style.css +++ b/capsulflask/static/style.css @@ -102,6 +102,9 @@ main { .row.grid-large > div { flex: 1 1 20em; } +.row.grid-medium > div { + flex: 1 1 13em; +} .row.grid-small > div { flex: 0 0 8em; } @@ -277,8 +280,12 @@ div.metric { border: 1px solid #777e73; background: #bdc7b810; } -.break-word { - word-break: break-word; +pre.code.wrap { + white-space: pre-wrap; +} + +.break-all { + word-break: break-all; } .dim { diff --git a/capsulflask/templates/capsul-detail.html b/capsulflask/templates/capsul-detail.html index 8636ef2..84afcdf 100644 --- a/capsulflask/templates/capsul-detail.html +++ b/capsulflask/templates/capsul-detail.html @@ -73,8 +73,8 @@ cyberian
      - - {{ vm['ssh_public_keys'] }} + + {{ vm['ssh_authorized_keys'] }}
      @@ -85,6 +85,15 @@
      +
      +

      ssh host key fingerprints

      +
      + +
      +
      {% for key in vm['ssh_host_keys'] %}
      +SHA256:{{ key.sha256 }} ({{ key.key_type }}){% endfor %}
      +
      +

      @@ -136,6 +145,17 @@ +
      +
      +
      +
      + add the following to your ~/.ssh/known_hosts file (optional) +
      +
      +
      {% for key in vm['ssh_host_keys'] %}
      +{{ vm['ipv4'] }} {{ key.content }}{% endfor %}
      +
      +
      {% endif %} {% endblock %} diff --git a/capsulflask/virt_model.py b/capsulflask/virt_model.py index e2b413b..1859a9f 100644 --- a/capsulflask/virt_model.py +++ b/capsulflask/virt_model.py @@ -1,5 +1,7 @@ import subprocess import re +import sys +import json from flask import current_app from time import sleep @@ -7,28 +9,23 @@ from os.path import join from subprocess import run from capsulflask.db import get_model +from capsulflask.shared import my_exec_info_message, VirtualMachine def validate_capsul_id(id): if not re.match(r"^(cvm|capsul)-[a-z0-9]{10}$", id): raise ValueError(f"vm id \"{id}\" must match \"^capsul-[a-z0-9]{{10}}$\"") -class VirtualMachine: - def __init__(self, id, ipv4=None, ipv6=None): - self.id = id - self.ipv4 = ipv4 - self.ipv6 = ipv6 - class VirtualizationInterface: def capacity_avaliable(self, additional_ram_bytes: int) -> bool: pass - def get(self, id: str) -> VirtualMachine: + def get(self, id: str, get_ssh_host_keys: bool) -> VirtualMachine: pass def list_ids(self) -> list: pass - def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory: int, ssh_public_keys: list): + def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory: int, ssh_authorized_keys: list): pass def destroy(self, email: str, id: str): @@ -38,14 +35,23 @@ class MockVirtualization(VirtualizationInterface): def capacity_avaliable(self, additional_ram_bytes): return True - def get(self, id): + def get(self, id, get_ssh_host_keys): validate_capsul_id(id) + + if get_ssh_host_keys: + ssh_host_keys = json.loads("""[ + {"key_type":"ED25519", "content":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN8cna0zeKSKl/r8whdn/KmDWhdzuWRVV0GaKIM+eshh", "sha256":"V4X2apAF6btGAfS45gmpldknoDX0ipJ5c6DLfZR2ttQ"}, + {"key_type":"RSA", "content":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvotgzgEP65JUQ8S8OoNKy1uEEPEAcFetSp7QpONe6hj4wPgyFNgVtdoWdNcU19dX3hpdse0G8OlaMUTnNVuRlbIZXuifXQ2jTtCFUA2mmJ5bF+XjGm3TXKMNGh9PN+wEPUeWd14vZL+QPUMev5LmA8cawPiU5+vVMLid93HRBj118aCJFQxLgrdP48VPfKHFRfCR6TIjg1ii3dH4acdJAvlmJ3GFB6ICT42EmBqskz2MPe0rIFxH8YohCBbAbrbWYcptHt4e48h4UdpZdYOhEdv89GrT8BF2C5cbQ5i9qVpI57bXKrj8hPZU5of48UHLSpXG8mbH0YDiOQOfKX/Mt", "sha256":"ghee6KzRnBJhND2kEUZSaouk7CD6o6z2aAc8GPkV+GQ"}, + {"key_type":"ECDSA", "content":"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLLgOoATz9R4aS2kk7vWoxX+lshK63t9+5BIHdzZeFE1o+shlcf0Wji8cN/L1+m3bi0uSETZDOAWMP3rHLJj9Hk=", "sha256":"aCYG1aD8cv/TjzJL0bi9jdabMGksdkfa7R8dCGm1yYs"} + ]""") + return VirtualMachine(id, ipv4="1.1.1.1", ssh_host_keys=ssh_host_keys) + return VirtualMachine(id, ipv4="1.1.1.1") def list_ids(self) -> list: return get_model().all_non_deleted_vm_ids() - def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list): + def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory_mb: int, ssh_authorized_keys: list): validate_capsul_id(id) current_app.logger.info(f"mock create: {id} for {email}") sleep(1) @@ -91,19 +97,31 @@ class ShellScriptVirtualization(VirtualizationInterface): return True - def get(self, id): + def get(self, id, get_ssh_host_keys): validate_capsul_id(id) completedProcess = run([join(current_app.root_path, 'shell_scripts/get.sh'), id], capture_output=True) self.validate_completed_process(completedProcess) - lines = completedProcess.stdout.splitlines() - if len(lines) == 0: + ipaddr_lines = completedProcess.stdout.splitlines() + if len(ipaddr_lines) == 0: return None - ipaddr = lines[0].decode("utf-8") + ipaddr = ipaddr_lines[0].decode("utf-8") if not re.match(r"^([0-9]{1,3}\.){3}[0-9]{1,3}$", ipaddr): return None + if get_ssh_host_keys: + try: + completedProcess2 = run([join(current_app.root_path, 'shell_scripts/ssh-keyscan.sh'), ipaddr], capture_output=True) + self.validate_completed_process(completedProcess2) + ssh_host_keys = json.loads(completedProcess2.stdout) + return VirtualMachine(id, ipv4=ipaddr, ssh_host_keys=ssh_host_keys) + except: + current_app.logger.warning(f""" + failed to ssh-keyscan {id} at {ipaddr}: + {my_exec_info_message(sys.exc_info())}""" + ) + return VirtualMachine(id, ipv4=ipaddr) def list_ids(self) -> list: @@ -111,13 +129,13 @@ class ShellScriptVirtualization(VirtualizationInterface): self.validate_completed_process(completedProcess) return list(map(lambda x: x.decode("utf-8"), completedProcess.stdout.splitlines() )) - def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory_mb: int, ssh_public_keys: list): + def create(self, email: str, id: str, template_image_file_name: str, vcpus: int, memory_mb: int, ssh_authorized_keys: list): validate_capsul_id(id) if not re.match(r"^[a-zA-Z0-9/_.-]+$", template_image_file_name): raise ValueError(f"template_image_file_name \"{template_image_file_name}\" must match \"^[a-zA-Z0-9/_.-]+$\"") - for ssh_public_key in ssh_public_keys: + for ssh_public_key in ssh_authorized_keys: if not re.match(r"^(ssh|ecdsa)-[0-9A-Za-z+/_=@. -]+$", ssh_public_key): raise ValueError(f"ssh_public_key \"{ssh_public_key}\" must match \"^(ssh|ecdsa)-[0-9A-Za-z+/_=@. -]+$\"") @@ -127,7 +145,7 @@ class ShellScriptVirtualization(VirtualizationInterface): if memory_mb < 512 or memory_mb > 16384: raise ValueError(f"memory_mb \"{memory_mb}\" must match 512 <= memory_mb <= 16384") - ssh_keys_string = "\n".join(ssh_public_keys) + ssh_keys_string = "\n".join(ssh_authorized_keys) current_app.logger.info(f"create vm virt model ssh_keys_string: {ssh_keys_string}") @@ -149,7 +167,7 @@ class ShellScriptVirtualization(VirtualizationInterface): template_image_file_name={template_image_file_name} vcpus={str(vcpus)} memory={str(memory_mb)} - ssh_public_keys={ssh_keys_string} + ssh_authorized_keys={ssh_keys_string} """ current_app.logger.info(f"create vm status: {status} vmSettings: {vmSettings}") From a7c61f01dffc48685d1edff6ae8374f8fac3b94f Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 01:42:27 -0600 Subject: [PATCH 12/41] remove extra debug logs --- capsulflask/console.py | 14 -------------- capsulflask/db_model.py | 2 -- capsulflask/static/style.css | 3 --- capsulflask/virt_model.py | 4 ---- 4 files changed, 23 deletions(-) diff --git a/capsulflask/console.py b/capsulflask/console.py index c161e7f..302644e 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -117,9 +117,6 @@ def detail(id): vm["created"] = vm['created'].strftime("%b %d %Y %H:%M") vm["ssh_authorized_keys"] = ", ".join(vm["ssh_authorized_keys"]) if len(vm["ssh_authorized_keys"]) > 0 else "" - - current_app.logger.info(f"asd {needs_ssh_host_keys} {json.dumps(vm['ssh_host_keys'])})") - return render_template( "capsul-detail.html", csrf_token = session["csrf-token"], @@ -139,10 +136,6 @@ def create(): account_balance = get_account_balance(get_vms(), get_payments(), datetime.utcnow()) capacity_avaliable = current_app.config["VIRTUALIZATION_MODEL"].capacity_avaliable(512*1024*1024) errors = list() - - ssh_keys_from_db_string = "\n".join(list(map(lambda x: f"name: {x['name']}**content: {x['content']}", public_keys_for_account))) - email_to_log = session["account"] - current_app.logger.info(f"create for {email_to_log}: ssh keys from db:\n {ssh_keys_from_db_string}") if request.method == "POST": if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: @@ -167,15 +160,11 @@ def create(): errors.append("something went wrong with ssh keys") else: for i in range(0, posted_keys_count): - to_log_bool = f"ssh_key_{i}" in request.form - current_app.logger.info(f"checking for ssh_key_{i}: {to_log_bool}") if f"ssh_key_{i}" in request.form: posted_name = request.form[f"ssh_key_{i}"] - current_app.logger.info(f"ssh key posted_name: {posted_name}") key = None for x in public_keys_for_account: if x['name'] == posted_name: - current_app.logger.info(f"ssh key posted_name {posted_name} was found") key = x if key: posted_keys.append(key) @@ -192,9 +181,6 @@ def create(): host(s) at capacity. no capsuls can be created at this time. sorry. """) - posted_keys_string = "\n".join(list(map(lambda x: f"name: {x['name']}**content: {x['content']}", posted_keys))) - current_app.logger.info(f"create for {email_to_log}: posted_keys:\n {posted_keys_string}") - if len(errors) == 0: id = makeCapsulId() get_model().create_vm( diff --git a/capsulflask/db_model.py b/capsulflask/db_model.py index fe362eb..82638f5 100644 --- a/capsulflask/db_model.py +++ b/capsulflask/db_model.py @@ -123,8 +123,6 @@ class DBModel: ) for ssh_authorized_key in ssh_authorized_keys: - current_app.logger.info(f"INSERT INTO vm_ssh_authorized_key (email, vm_id, ssh_public_key_name) VALUES (\"{email}\", \"{id}\", \"{ssh_authorized_key}\")") - self.cursor.execute(""" INSERT INTO vm_ssh_authorized_key (email, vm_id, ssh_public_key_name) VALUES (%s, %s, %s) diff --git a/capsulflask/static/style.css b/capsulflask/static/style.css index 4ad29f6..9382ef3 100644 --- a/capsulflask/static/style.css +++ b/capsulflask/static/style.css @@ -102,9 +102,6 @@ main { .row.grid-large > div { flex: 1 1 20em; } -.row.grid-medium > div { - flex: 1 1 13em; -} .row.grid-small > div { flex: 0 0 8em; } diff --git a/capsulflask/virt_model.py b/capsulflask/virt_model.py index 1859a9f..0a15c23 100644 --- a/capsulflask/virt_model.py +++ b/capsulflask/virt_model.py @@ -147,8 +147,6 @@ class ShellScriptVirtualization(VirtualizationInterface): ssh_keys_string = "\n".join(ssh_authorized_keys) - current_app.logger.info(f"create vm virt model ssh_keys_string: {ssh_keys_string}") - completedProcess = run([ join(current_app.root_path, 'shell_scripts/create.sh'), id, @@ -170,8 +168,6 @@ class ShellScriptVirtualization(VirtualizationInterface): ssh_authorized_keys={ssh_keys_string} """ - current_app.logger.info(f"create vm status: {status} vmSettings: {vmSettings}") - if not status == "success": raise ValueError(f"""failed to create vm for {email} with: {vmSettings} From 71222e07e6591e20e68920a99c7c50e517f2b831 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 01:51:52 -0600 Subject: [PATCH 13/41] chmod +x capsulflask/shell_scripts/ssh-keyscan.sh --- capsulflask/shell_scripts/ssh-keyscan.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 capsulflask/shell_scripts/ssh-keyscan.sh diff --git a/capsulflask/shell_scripts/ssh-keyscan.sh b/capsulflask/shell_scripts/ssh-keyscan.sh old mode 100644 new mode 100755 From e18d15f7e709fb4db5da41ca01e4b5fc38eb3968 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 01:57:52 -0600 Subject: [PATCH 14/41] forgot to decode ssh-keyscan.sh output to string --- capsulflask/virt_model.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/capsulflask/virt_model.py b/capsulflask/virt_model.py index 0a15c23..8620fe4 100644 --- a/capsulflask/virt_model.py +++ b/capsulflask/virt_model.py @@ -114,7 +114,11 @@ class ShellScriptVirtualization(VirtualizationInterface): try: completedProcess2 = run([join(current_app.root_path, 'shell_scripts/ssh-keyscan.sh'), ipaddr], capture_output=True) self.validate_completed_process(completedProcess2) - ssh_host_keys = json.loads(completedProcess2.stdout) + current_app.logger.warning(f""" + failed to ssh-keyscan: {completedProcess2.stdout.decode("utf-8")} + """ + ) + ssh_host_keys = json.loads(completedProcess2.stdout.decode("utf-8")) return VirtualMachine(id, ipv4=ipaddr, ssh_host_keys=ssh_host_keys) except: current_app.logger.warning(f""" From 485a17ae6b7b4e9d9072e1e327297d257980ffa7 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 02:03:29 -0600 Subject: [PATCH 15/41] fix ssh-keyscan.sh newlines printf --- capsulflask/shell_scripts/ssh-keyscan.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/capsulflask/shell_scripts/ssh-keyscan.sh b/capsulflask/shell_scripts/ssh-keyscan.sh index c730a84..909ac40 100755 --- a/capsulflask/shell_scripts/ssh-keyscan.sh +++ b/capsulflask/shell_scripts/ssh-keyscan.sh @@ -8,15 +8,16 @@ if echo "$ip_address" | grep -vqE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then fi printf '[' -DELIMITER="\n" +DELIMITER="" ssh-keyscan "$ip_address" 2>/dev/null | while read -r line; do if echo "$line" | grep -qE "^$ip_address"' +(ssh|ecdsa)-[0-9A-Za-z+/_=@. -]+$'; then KEY_CONTENT="$(echo "$line" | awk '{ print $2 " " $3 }')" FINGERPRINT_OUTPUT="$(echo "$KEY_CONTENT" | ssh-keygen -l -E sha256 -f - | sed -E 's/^[0-9]+ SHA256:([0-9A-Za-z+/-]+) .+ \(([A-Z0-9]+)\)$/\1 \2/g')" SHA256_HASH="$(echo "$FINGERPRINT_OUTPUT" | awk '{ print $1 }')" KEY_TYPE="$(echo "$FINGERPRINT_OUTPUT" | awk '{ print $2 }')" - printf '%s{"key_type":"%s", "content":"%s", "sha256":"%s"}' "$DELIMITER" "$KEY_TYPE" "$KEY_CONTENT" "$SHA256_HASH" - DELIMITER=",\n" + printf '%s\n {"key_type":"%s", "content":"%s", "sha256":"%s"}' "$DELIMITER" "$KEY_TYPE" "$KEY_CONTENT" "$SHA256_HASH" + DELIMITER="," fi done printf '\n]\n' + From 39980d836b17eb0bd6224764aafc513cd6761c8f Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 02:05:06 -0600 Subject: [PATCH 16/41] remove debug log --- capsulflask/virt_model.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/capsulflask/virt_model.py b/capsulflask/virt_model.py index 8620fe4..1043efc 100644 --- a/capsulflask/virt_model.py +++ b/capsulflask/virt_model.py @@ -114,10 +114,6 @@ class ShellScriptVirtualization(VirtualizationInterface): try: completedProcess2 = run([join(current_app.root_path, 'shell_scripts/ssh-keyscan.sh'), ipaddr], capture_output=True) self.validate_completed_process(completedProcess2) - current_app.logger.warning(f""" - failed to ssh-keyscan: {completedProcess2.stdout.decode("utf-8")} - """ - ) ssh_host_keys = json.loads(completedProcess2.stdout.decode("utf-8")) return VirtualMachine(id, ipv4=ipaddr, ssh_host_keys=ssh_host_keys) except: From 280bcfd584b87de4038ac07ef448ff48b6bae6de Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 02:10:22 -0600 Subject: [PATCH 17/41] =?UTF-8?q?check=20null=20on=20double=5Fcheck=5Fcaps?= =?UTF-8?q?ul=5Faddress=20=F0=9F=98=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- capsulflask/console.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/capsulflask/console.py b/capsulflask/console.py index 302644e..ee36f98 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -40,6 +40,7 @@ def double_check_capsul_address(id, ipv4, get_ssh_host_keys): the virtualization model threw an error in double_check_capsul_address of {id}: {my_exec_info_message(sys.exc_info())}""" ) + return None return result @@ -57,7 +58,9 @@ def index(): # for now we are going to check the IP according to the virt model # on every request. this could be done by a background job and cached later on... for vm in vms: - vm["ipv4"] = double_check_capsul_address(vm["id"], vm["ipv4"], False).ipv4 + result = double_check_capsul_address(vm["id"], vm["ipv4"], False) + if result is not None: + vm["ipv4"] = result.ipv4 vms = list(map( lambda x: dict( @@ -109,10 +112,13 @@ def detail(id): else: needs_ssh_host_keys = "ssh_host_keys" not in vm or len(vm["ssh_host_keys"]) == 0 + vm_from_virt_model = double_check_capsul_address(vm["id"], vm["ipv4"], needs_ssh_host_keys) - vm["ipv4"] = vm_from_virt_model.ipv4 - if needs_ssh_host_keys: - vm["ssh_host_keys"] = vm_from_virt_model.ssh_host_keys + + if vm_from_virt_model is not None: + vm["ipv4"] = vm_from_virt_model.ipv4 + if needs_ssh_host_keys: + vm["ssh_host_keys"] = vm_from_virt_model.ssh_host_keys vm["created"] = vm['created'].strftime("%b %d %Y %H:%M") vm["ssh_authorized_keys"] = ", ".join(vm["ssh_authorized_keys"]) if len(vm["ssh_authorized_keys"]) > 0 else "" From c849ec3c11a03eb67e931e918c62482f632d784e Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 15:27:24 -0600 Subject: [PATCH 18/41] add 25 cent buffer to affordable_vm_sizes logic for UX --- capsulflask/console.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/capsulflask/console.py b/capsulflask/console.py index ee36f98..b80779e 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -209,7 +209,10 @@ def create(): affordable_vm_sizes = dict() for key, vm_size in vm_sizes.items(): - if vm_size["dollars_per_month"] <= account_balance: + # if a user deposits $7.50 and then creates an f1-s vm which costs 7.50 a month, + # then they have to delete the vm and re-create it, they will not be able to, they will have to pay again. + # so for UX it makes a lot of sense to give a small margin of 25 cents for usability sake + if vm_size["dollars_per_month"] <= account_balance+0.25: affordable_vm_sizes[key] = vm_size for error in errors: From 7217ff7a6d3d76b31d52613162620390fc3b9dc2 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 22:14:04 -0600 Subject: [PATCH 19/41] add /about-ssh essay --- capsulflask/landing.py | 4 + capsulflask/static/style.css | 18 +- capsulflask/templates/about-ssh.html | 340 +++++++++++++++++++++++ capsulflask/templates/capsul-detail.html | 4 +- 4 files changed, 362 insertions(+), 4 deletions(-) create mode 100644 capsulflask/templates/about-ssh.html diff --git a/capsulflask/landing.py b/capsulflask/landing.py index 0724225..3c1781e 100644 --- a/capsulflask/landing.py +++ b/capsulflask/landing.py @@ -22,6 +22,10 @@ def pricing(): def faq(): return render_template("faq.html") +@bp.route("/about-ssh") +def about_ssh(): + return render_template("about-ssh.html") + @bp.route("/changelog") def changelog(): return render_template("changelog.html") diff --git a/capsulflask/static/style.css b/capsulflask/static/style.css index 9382ef3..0d01de0 100644 --- a/capsulflask/static/style.css +++ b/capsulflask/static/style.css @@ -211,10 +211,22 @@ input[type=image].submit { } - - -ul li { +ul li, ol li { margin: 0.5em 0; + margin-left: 1.2rem; +} + +.long-form p, .long-form li { + line-height: 2em; +} + +.long-form p .code, .long-form li .code{ + line-height: 1em; + padding: 5px; + padding-top: 3px; + margin-top: 2px; + padding-bottom: 4px; + border-radius: 4px; } hr { diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html new file mode 100644 index 0000000..acb0d57 --- /dev/null +++ b/capsulflask/templates/about-ssh.html @@ -0,0 +1,340 @@ +{% extends 'base.html' %} + +{% block title %}About SSH{% endblock %} + +{% block content %} +

      Understanding the Secure Shell Protocol (SSH)

      +{% endblock %} + +{% block subcontent %} +
      +

      + In order to use our service, you will have to use the Secure Shell protocol (SSH) to connect to your capsul. +

      +

      + SSH is a very old tool, created back when the internet was a different place, with different use cases and concerns. + In many ways, the protocol has failed to evolve to meet the needs of our 21st century global internet. + Instead, the users of SSH (tech heads, sysadmins, etc) have had to evolve our processes to work around SSH's limitations. +

      +

      + These days, we use SSH + public-key cryptography to establish secure connections to our servers. + If you are not familiar with the concept of public key cryptography, cryptographic signatures, + or diffie-hellman key exchange, you may wish to see + the wikipedia article for a refresher. +

      + +

      Public Key Crypto and Key Exchange: The TL;DR

      + +

      + Computers can generate "key pairs" which consist of a public key and a private key. Given a public key pair A: +

        +
      1. + A computer which has access to public key A can encrypt data, + and then ONLY a computer which has access private key A can decrypt & read it +
      2. +
      3. + Likewise, a computer which has access to private key A can encrypt data, + and any a computer which has access public key A can decrypt it, + thus PROVING the message must have come from someone who posesses private key A +
      4. +
      + Key exchange is a process in which two computers, Computer A and Computer B (often referred to as Alice and Bob) + both create key pairs, so you have key pair A and key pair B, for a total of 4 keys: +
        +
      1. public key A
      2. +
      3. private key A
      4. +
      5. public key B
      6. +
      7. private key B
      8. +
      + In simplified terms, during a key exchange, +
        +
      1. computer A sends computer B its public key
      2. +
      3. computer B sends computer A its public key
      4. +
      5. computer A sends computer B + a message which is encrypted with computer B's public key
      6. +
      7. computer B sends computer A + a message which is encrypted with computer A's public key
      8. +
      + The way this process is carried out allows A and B to communicate with each-other securely, which is great,

      + + HOWEVER, there is a catch!! +

      + +

      + When computers A and B are trying to establish a secure connection for the first time, + we assume that the way they communicate right now is NOT secure. That means that someone between A and B can read & modify + all messages they send to each-other! You might be able to see where this is heading... +

      +

      + When computer A sends its public key to computer B, + someone in the middle (lets call it computer E, or Eve) could record that message, save it, + and then replace it with a forged message to computer B containing public key E + (from a key pair that computer E generated). + + If this happens, when computer B sends an encrypted message to computer A, + B thinks that A's public key is actually public key E, so it will use public key E to encrypt. + And again, computer E in the middle can intercept the message, and they can decrypt it as well + because they have private key E. + Finally, they can relay the same message to computer A, this time encrypted with computer A's public key. + This is called a Man In The Middle (MITM) attack. +

      +

      + Without some additional verification method, + Computer A AND Computer B can both be duped and the connection is NOT really secure. +

      + +

      Authenticating Public Keys: A Tale of Two Protocols

      + +

      + Now that we have seen how key exhange works, + and we understand that in order to prevent MITM attacks, all participants have to have a way of knowing + whether a given public key is authentic or not, I can explain what I meant when I said +

      +

      + > [SSH] has failed to evolve to meet the needs of our 21st century global internet +

      +

      + In order to explain this, let's first look at how a different, more modern protocol, + Transport Layer Security (or TLS) solved this problem. + TLS, (still sometimes called by its olde name "Secure Sockets Layer", or SSL) was created to enable HTTPS, and allow + internet users to log into web sites securely and purchase things online by entering their credit card number. + Of course, this required security that actually works; if someone could MITM attack the connection, they could easily + steal tons of credit card numbers and passwords. +

      +

      + In order to enable this, a new sub-protocol called X.509 was created. + X.509 is a standard related to the data format of certificates and keys (public keys and private keys), but it also defines + a simple and easy way to determine whether a given certificate (public key) is authentic. + X.509 introduced the concept of a Certificate Authority, or CA. + These CAs were supposed to be bank-like public institutions of power which everyone could trust. + The CA would create a key pair on an extremely secure computer, and then a CA Certificate (the public side of that key pair) + would be distributed along with every copy of Windows, Mac OS, and Linux. Then companies who wanted to run a secure web server + could generate thier OWN key pair for thier web server, + and pay the CA to sign thier web server's X.509 certificate (public key) with the highly protected CA private key. + Critically, issue date, expiration date, and the domain name of the web server, like foo.example.com, would have to be included + in the x.509 certiciate along with the public key. + This way, when the user types https://foo.example.com into thier web browser: + +

        +
      1. The web browser sends a TLS ClientHello request to the server
      2. +
      3. + The server responds with a ServerHello & ServerCertificate message +
          +
        • The ServerCertificate message contains the X.509 certificate for the web server at foo.example.com
        • +
        +
      4. +
      5. The web browser inspects the X.509 certificate +
          +
        • + Is the current date in between the issued date and expiry date of the certificate? + If not, display an EXPIRED_CERTIFICATE error. +
        • +
        • + Does the domain name the user typed in, foo.example.com, match the domain name in the certificate? + If not, display a BAD_CERT_DOMAIN error. +
        • +
        • + Does the certificate contain a valid CA signature? + (can the signature on the certificate be decrypted by one of the CA Certificates included with the operating system?) + If not, display a UNKNOWN_ISSUER error. +
        • +
        +
      6. +
      7. Assuming all the checks pass, the web browser trusts the certificate and connects
      8. +
      +

      +

      + This system enabled the internet to grow and flourish: + purchasing from a CA was the only way to get a valid X.509 certificate for a website, + and guaranteeing authenticity was in the CA's business interest. + The CAs kept their private keys behind razor wire and armed guards, and followed strict rules to ensure that only the right + people got thier certificates signed. + Only the CA's themselves or anyone who had enough power to force them to create a fraudulent certificate + would be able to execute MITM attacks. +

      +

      + The TLS+X.509 Certificate Authority works well for HTTP and other application protocols, because +

        +
      • Most internet users don't have the patience to manually verify the authenticity of digital certificates.
      • +
      • Most internet users don't understand or care how it works; they just want to connect right now.
      • +
      • Businesses and organizations that run websites are generally willing to jump through hoops and + subjugate themselves to authorities in order to offer a more secure application experience to thier users.
      • +
      • The centralization & problematic power dynamic which CAs represent + is easily swept under the rug, if it doesn't directly or noticably impact the average person, who cares?
      • +
      +

      + +

      + However, this would never fly with SSH. You have to understand, SSH does not come from Microsoft, it does not come from Apple, + in fact, it does not even come from Linux or GNU. SSH comes from BSD. + Berkeley Software Distribution. Most people don't even know + what BSD is. It's Deep Nerdcore material. The people who maintain SSH are not playing around, they would never + allow themselves to be subjugated by so-called "Certificate Authorities". + So, what are they doing instead? Where is SSH at? Well, back when it was created, computer security was easy — + a very minimal defense was enough to deter attackers. + In order to help prevent these MITM attacks, instead of something like X.509, SSH uses a policy called + Trust On First Use (TOFU). +

      + +

      + The SSH client application keeps a record of every server it has ever connected to + in a file ~/.ssh/known_hosts. +

      + +

      + (the tilde ~ here represents the user's home directory, + /home/username on linux, + C:\Users\username on Windows, and + /Users/username on MacOS). +

      + +

      + If the user asks the SSH client to connect to a server it has never seen before, + it will print a prompt like this to the terminal: +

      + +
      The authenticity of host 'fooserver.com (69.4.20.69)' can't be established.
      +    ECDSA key fingerprint is SHA256:EXAMPLE1xY4JUVhYirOVlfuDFtgTbaiw3x29xYizEeU.
      +    Are you sure you want to continue connecting (yes/no/[fingerprint])?
      + +

      + Here, the SSH client is displaying the fingerprint (SHA256 hash) + of the public key provided by the server at fooserver.com. + Back in the day, when SSH was created, servers lived for months to years, not minutes, and they were installed by hand. + So it would have been perfectly reasonable to call the person installing the server + and ask them to log into it & read off the host key fingerprint over the phone. + After verifing that the fingerprints match in the phone call, the user would type yes + to continue. +

      + +

      + After the SSH client connects to a server for the first time, it will record the server's IP address and public key in the + ~/.ssh/known_hosts file. All subsequent connections will simply check the public key + the server presents against the public key it has recorded in the ~/.ssh/known_hosts file. + If the two public keys match, the connection will continue without prompting the user, however, if they don't match, + the SSH client will display a scary warning message: +

      +
      +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
      +@       WARNING: POSSIBLE DNS SPOOFING DETECTED!          @
      +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
      +The ECDSA host key for fooserver.com has changed,
      +and the key for the corresponding IP address 69.4.20.42
      +is unknown. This could either mean that
      +DNS SPOOFING is happening or the IP address for the host
      +and its host key have changed at the same time.
      +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
      +@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
      +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
      +IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
      +Someone could be eavesdropping on you right now (man-in-the-middle attack)!
      +It is also possible that a host key has just been changed.
      +The fingerprint for the ECDSA key sent by the remote host is
      +SHA256:EXAMPLEpDDefcNcIROtFpuTiHC1j3iNU74aaKFO03+0.
      +Please contact your system administrator.
      +Add correct host key in /root/.ssh/known_hosts to get rid of this message.
      +Offending ECDSA key in /root/.ssh/known_hosts:1
      +  remove with:
      +  ssh-keygen -f "/root/.ssh/known_hosts" -R "fooserver.com"
      +ECDSA host key for fooserver.com has changed and you have requested strict checking.
      +Host key verification failed.
      +
      + +

      + This is why it's called Trust On First Use: + + SSH protocol assumes that when you type yes in response to the prompt during your first connection, + you really did verify that the server's public key fingerprint matches. + + If you type yes here without checking the server's host key somehow, you could add an attackers public key to the trusted + list in your ~/.ssh/known_hosts file; if you type yes blindly, you are + completely disabling all security of the SSH connection. + It can be fully man-in-the-middle attacked & you are vulnerable to spying, command injection, result-falsification, + the whole nine yards. +

      + +

      + So what are technologists to do? Most cloud providers don't "provide" a secure and reliable way to get the SSH host public keys + for instances that users create on thier platform. For example, see this + + question posted by a frustrated user trying to secure thier connection to a digitalocean droplet + . + + Besides using the provider's HTTPS-based console to log into the machine & directly read the public key, most of the time, + providers recommend using a "userdata script", which runs when the machine boots, to upload the machine's SSH public keys to a + trusted location, like Backblaze B2 or + Amazon S3[1] storage for later retrieval by users. + As an example, I wrote a + + userdata script which does this + for my own cloud compute management tool called + rootsystem. + Later in the process, rootsystem will + + download the public keys from the Object Storage provider + and add them to the ~/.ssh/known_hosts file + before finally + + invoking the ssh client against the cloud host. +

      + +

      + Personally, I think it's disgusting and irresponsible to require users to go through that much work + just to be able to connect to their instance securely. However, this practice appears to be an industry standard. + It's gross, but it's where we're at right now. +

      + +

      + So for capsul, we obviously wanted to do better. + We wanted to make this kind of thing as easy as possible for the user, + so I'm proud to announce as of today, capsul SSH host key fingerprints will be displayed on the capsul detail page, + as well as the host's SSH public keys themselves in ~/.ssh/known_hosts format. + Users can simply copy and paste these keys into thier ~/.ssh/known_hosts file and connect + with confidence that they are not being MITM attacked. +

      + +

      It's 2021. Can't we do better than this? What's next?

      + +

      + Glad you asked 😜. +

      +

      + TLS is great, except it has one problem: the X.509 CA system centralizes power and structurally invites abuse. + Power corrupts, and absolute power corrupts absolutely. But there is hope for the future: with the invention of Bitcoin + in 2009, we now have a new tool to use for authority-free secure consensus. Some bright folks have forked Bitcoin to produce + Namecoin, a DNS-like public blockchain which is + merge-mined with Bitcoin, and which allows users to + + register and trade names, including domain names + . In fact, Namecoin features a + + specification for associating public keys with domain names + + and easy-to-use client software packages capable of resolving these + names + & + + public + + + keys, + capable of replacing both the DNS system and X.509 Certificate Authority system. +

      + +

      + For more information on how to get started with Namecoin, see my + + Namecoin guide for webmasters. + +

      + +
      +

      + [1] fuck amazon +

      + +
      +{% endblock %} + +{% block pagesource %}/templates/about-ssh.html{% endblock %} + diff --git a/capsulflask/templates/capsul-detail.html b/capsulflask/templates/capsul-detail.html index 84afcdf..b08e020 100644 --- a/capsulflask/templates/capsul-detail.html +++ b/capsulflask/templates/capsul-detail.html @@ -93,7 +93,9 @@
      {% for key in vm['ssh_host_keys'] %}
       SHA256:{{ key.sha256 }} ({{ key.key_type }}){% endfor %}
      - +

      From 0b5583a0ed67896bac9f87cb94471478e62e06d0 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 22:15:34 -0600 Subject: [PATCH 20/41] make link to about-ssh be a paragraph --- capsulflask/templates/capsul-detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capsulflask/templates/capsul-detail.html b/capsulflask/templates/capsul-detail.html index b08e020..16588ff 100644 --- a/capsulflask/templates/capsul-detail.html +++ b/capsulflask/templates/capsul-detail.html @@ -94,7 +94,7 @@ SHA256:{{ key.sha256 }} ({{ key.key_type }}){% endfor %}

      From cf9bcd19124ebcf030d40b6086e62314c642b0d7 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 22:17:09 -0600 Subject: [PATCH 21/41] duplicate links to /about-ssh --- capsulflask/templates/capsul-detail.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/capsulflask/templates/capsul-detail.html b/capsulflask/templates/capsul-detail.html index 16588ff..91a87b6 100644 --- a/capsulflask/templates/capsul-detail.html +++ b/capsulflask/templates/capsul-detail.html @@ -94,7 +94,7 @@ SHA256:{{ key.sha256 }} ({{ key.key_type }}){% endfor %}

      @@ -158,6 +158,9 @@ SHA256:{{ key.sha256 }} ({{ key.key_type }}){% endfor %} {{ vm['ipv4'] }} {{ key.content }}{% endfor %}
      + {% endif %} {% endblock %} From d85ebd9042743e34c88b2c6cb6eba965c0620502 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 22:20:26 -0600 Subject: [PATCH 22/41] "on the network between A and B " --- capsulflask/templates/about-ssh.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index acb0d57..947e18c 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -62,7 +62,8 @@

      When computers A and B are trying to establish a secure connection for the first time, - we assume that the way they communicate right now is NOT secure. That means that someone between A and B can read & modify + we assume that the way they communicate right now is NOT secure. That means that someone on the network + between A and B can read & modify all messages they send to each-other! You might be able to see where this is heading...

      From 113e6e9339f2331242c5b40da99aa2da6728d951 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 22:22:32 -0600 Subject: [PATCH 23/41] link to https://en.wikipedia.org/wiki/Man-in-the-middle_attack --- capsulflask/templates/about-ssh.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index 947e18c..d75a76c 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -77,7 +77,7 @@ And again, computer E in the middle can intercept the message, and they can decrypt it as well because they have private key E. Finally, they can relay the same message to computer A, this time encrypted with computer A's public key. - This is called a Man In The Middle (MITM) attack. + This is called a Man In The Middle (MITM) attack.

      Without some additional verification method, From f56135c5992f300308fc141fb2b22c60cb38cbcd Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 22:24:12 -0600 Subject: [PATCH 24/41] more wikipedia links! --- capsulflask/templates/about-ssh.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index d75a76c..63fcd5d 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -96,7 +96,7 @@

      In order to explain this, let's first look at how a different, more modern protocol, - Transport Layer Security (or TLS) solved this problem. + Transport Layer Security (or TLS) solved this problem. TLS, (still sometimes called by its olde name "Secure Sockets Layer", or SSL) was created to enable HTTPS, and allow internet users to log into web sites securely and purchase things online by entering their credit card number. Of course, this required security that actually works; if someone could MITM attack the connection, they could easily From a9bf2778427658f595cd9cff067f77600f46f5b9 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 22:25:13 -0600 Subject: [PATCH 25/41] more wikipedia links --- capsulflask/templates/about-ssh.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index 63fcd5d..8d8a80d 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -12,7 +12,7 @@ In order to use our service, you will have to use the Secure Shell protocol (SSH) to connect to your capsul.

      - SSH is a very old tool, created back when the internet was a different place, with different use cases and concerns. + SSH is a very old tool, created back when the internet was a different place, with different use cases and concerns. In many ways, the protocol has failed to evolve to meet the needs of our 21st century global internet. Instead, the users of SSH (tech heads, sysadmins, etc) have had to evolve our processes to work around SSH's limitations.

      From 01a31be0913ce8652e86bd352bd7d01514a9773f Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 22:30:12 -0600 Subject: [PATCH 26/41] signed, forest --- capsulflask/templates/about-ssh.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index 8d8a80d..a6d7cbe 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -109,7 +109,7 @@ X.509 introduced the concept of a Certificate Authority, or CA. These CAs were supposed to be bank-like public institutions of power which everyone could trust. The CA would create a key pair on an extremely secure computer, and then a CA Certificate (the public side of that key pair) - would be distributed along with every copy of Windows, Mac OS, and Linux. Then companies who wanted to run a secure web server + would be distributed along with every copy of Windows, Mac OS, and Linux. Then folks who wanted to run a secure web server could generate thier OWN key pair for thier web server, and pay the CA to sign thier web server's X.509 certificate (public key) with the highly protected CA private key. Critically, issue date, expiration date, and the domain name of the web server, like foo.example.com, would have to be included @@ -258,8 +258,7 @@ Host key verification failed. So what are technologists to do? Most cloud providers don't "provide" a secure and reliable way to get the SSH host public keys for instances that users create on thier platform. For example, see this - question posted by a frustrated user trying to secure thier connection to a digitalocean droplet - . + question posted by a frustrated user trying to secure thier connection to a digitalocean droplet. Besides using the provider's HTTPS-based console to log into the machine & directly read the public key, most of the time, providers recommend using a "userdata script", which runs when the machine boots, to upload the machine's SSH public keys to a @@ -326,7 +325,11 @@ Host key verification failed. For more information on how to get started with Namecoin, see my Namecoin guide for webmasters. +

      +

      + Cheers and best wishes,
      +         Forest


      From bef26c38cf4019e5a6cbca6578e553f27a086098 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 30 Jan 2021 22:41:20 -0600 Subject: [PATCH 27/41] dont put lists inside

      tags --- capsulflask/templates/about-ssh.html | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index a6d7cbe..b581ed0 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -27,6 +27,7 @@

      Computers can generate "key pairs" which consist of a public key and a private key. Given a public key pair A: +

      1. A computer which has access to public key A can encrypt data, @@ -38,15 +39,19 @@ thus PROVING the message must have come from someone who posesses private key A
      +

      Key exchange is a process in which two computers, Computer A and Computer B (often referred to as Alice and Bob) both create key pairs, so you have key pair A and key pair B, for a total of 4 keys: +

      1. public key A
      2. private key A
      3. public key B
      4. private key B
      +

      In simplified terms, during a key exchange, +

      1. computer A sends computer B its public key
      2. computer B sends computer A its public key
      3. @@ -55,6 +60,7 @@
      4. computer B sends computer A a message which is encrypted with computer A's public key
      +

      The way this process is carried out allows A and B to communicate with each-other securely, which is great,

      HOWEVER, there is a catch!! @@ -115,7 +121,7 @@ Critically, issue date, expiration date, and the domain name of the web server, like foo.example.com, would have to be included in the x.509 certiciate along with the public key. This way, when the user types https://foo.example.com into thier web browser: - +

      1. The web browser sends a TLS ClientHello request to the server
      2. @@ -143,7 +149,6 @@
      3. Assuming all the checks pass, the web browser trusts the certificate and connects
      -

      This system enabled the internet to grow and flourish: purchasing from a CA was the only way to get a valid X.509 certificate for a website, @@ -155,6 +160,7 @@

      The TLS+X.509 Certificate Authority works well for HTTP and other application protocols, because +

      • Most internet users don't have the patience to manually verify the authenticity of digital certificates.
      • Most internet users don't understand or care how it works; they just want to connect right now.
      • @@ -163,7 +169,6 @@
      • The centralization & problematic power dynamic which CAs represent is easily swept under the rug, if it doesn't directly or noticably impact the average person, who cares?
      -

      However, this would never fly with SSH. You have to understand, SSH does not come from Microsoft, it does not come from Apple, From a4f39ca018b88c2da7ccfccff5b5648ac501cf29 Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 00:21:25 -0600 Subject: [PATCH 28/41] fix whitespace --- capsulflask/templates/about-ssh.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index b581ed0..b984d71 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -310,8 +310,8 @@ Host key verification failed. Namecoin, a DNS-like public blockchain which is merge-mined with Bitcoin, and which allows users to - register and trade names, including domain names - . In fact, Namecoin features a + register and trade names, including domain names. + In fact, Namecoin features a specification for associating public keys with domain names From ebaf348dd3b76da085904ac80236fd64374c55b5 Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 00:25:06 -0600 Subject: [PATCH 29/41] fix grammer --- capsulflask/templates/about-ssh.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index b984d71..28caae3 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -103,8 +103,8 @@

      In order to explain this, let's first look at how a different, more modern protocol, Transport Layer Security (or TLS) solved this problem. - TLS, (still sometimes called by its olde name "Secure Sockets Layer", or SSL) was created to enable HTTPS, and allow - internet users to log into web sites securely and purchase things online by entering their credit card number. + TLS, (still sometimes called by its olde name "Secure Sockets Layer", or SSL) was created to enable HTTPS, allow + internet users to log into web sites securely, and purchase things online by entering their credit card number. Of course, this required security that actually works; if someone could MITM attack the connection, they could easily steal tons of credit card numbers and passwords.

      From 7a904114c3c6b307b0a72423d7a96d5369d6bbf4 Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 00:57:33 -0600 Subject: [PATCH 30/41] mor grammer fixs --- capsulflask/templates/about-ssh.html | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index 28caae3..d1bc749 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -109,8 +109,8 @@ steal tons of credit card numbers and passwords.

      - In order to enable this, a new sub-protocol called X.509 was created. - X.509 is a standard related to the data format of certificates and keys (public keys and private keys), but it also defines + In order to enable this, a new standard called X.509 was created. + X.509 dictates the data format of certificates and keys (public keys and private keys), and it also defines a simple and easy way to determine whether a given certificate (public key) is authentic. X.509 introduced the concept of a Certificate Authority, or CA. These CAs were supposed to be bank-like public institutions of power which everyone could trust. @@ -143,7 +143,7 @@

    • Does the certificate contain a valid CA signature? (can the signature on the certificate be decrypted by one of the CA Certificates included with the operating system?) - If not, display a UNKNOWN_ISSUER error. + If not, display an UNKNOWN_ISSUER error.
    @@ -178,7 +178,7 @@ allow themselves to be subjugated by so-called "Certificate Authorities". So, what are they doing instead? Where is SSH at? Well, back when it was created, computer security was easy — a very minimal defense was enough to deter attackers. - In order to help prevent these MITM attacks, instead of something like X.509, SSH uses a policy called + In order to help prevent these MITM attacks, instead of something like X.509, SSH employs a policy called Trust On First Use (TOFU).

    @@ -207,7 +207,8 @@ Here, the SSH client is displaying the fingerprint (SHA256 hash) of the public key provided by the server at fooserver.com. Back in the day, when SSH was created, servers lived for months to years, not minutes, and they were installed by hand. - So it would have been perfectly reasonable to call the person installing the server + So it would have been perfectly reasonable to call the person installing the server on thier + Nokia 909 and ask them to log into it & read off the host key fingerprint over the phone. After verifing that the fingerprints match in the phone call, the user would type yes to continue. @@ -255,8 +256,8 @@ Host key verification failed. If you type yes here without checking the server's host key somehow, you could add an attackers public key to the trusted list in your ~/.ssh/known_hosts file; if you type yes blindly, you are completely disabling all security of the SSH connection. - It can be fully man-in-the-middle attacked & you are vulnerable to spying, command injection, result-falsification, - the whole nine yards. + It can be fully man-in-the-middle attacked & you are + vulnerable to surveillance, command injection, even emulation/falsification of the entire stream.

    From 57666cf2abae10df3decfc187194f1e1e8325913 Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 01:18:15 -0600 Subject: [PATCH 31/41] more grammar & writing edits --- capsulflask/templates/about-ssh.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index d1bc749..ea81d87 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -261,15 +261,16 @@ Host key verification failed.

    - So what are technologists to do? Most cloud providers don't "provide" a secure and reliable way to get the SSH host public keys + So what are technologists to do? Most cloud providers don't "provide" an easy way to get the SSH host public keys for instances that users create on thier platform. For example, see this question posted by a frustrated user trying to secure thier connection to a digitalocean droplet. - Besides using the provider's HTTPS-based console to log into the machine & directly read the public key, most of the time, - providers recommend using a "userdata script", which runs when the machine boots, to upload the machine's SSH public keys to a - trusted location, like Backblaze B2 or - Amazon S3[1] storage for later retrieval by users. + Besides using the provider's HTTPS-based console to log into the machine & directly read the public key, + providers also recommend using a "userdata script". + This script would run on boot & to upload the machine's SSH public keys to a + trusted location like Backblaze B2 or + Amazon S3[1], for an application to retrieve later. As an example, I wrote a userdata script which does this From a785392b004edb62edeb6f17ba984fed54125593 Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 01:19:04 -0600 Subject: [PATCH 32/41] blah --- capsulflask/templates/about-ssh.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index ea81d87..df65681 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -268,7 +268,7 @@ Host key verification failed. Besides using the provider's HTTPS-based console to log into the machine & directly read the public key, providers also recommend using a "userdata script". - This script would run on boot & to upload the machine's SSH public keys to a + This script would run on boot & upload the machine's SSH public keys to a trusted location like Backblaze B2 or Amazon S3[1], for an application to retrieve later. As an example, I wrote a From 658f8bf3164068df94b3f10b500d6f457936a783 Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 01:22:23 -0600 Subject: [PATCH 33/41] more fix wording --- capsulflask/templates/about-ssh.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index df65681..619e482 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -103,8 +103,8 @@

    In order to explain this, let's first look at how a different, more modern protocol, Transport Layer Security (or TLS) solved this problem. - TLS, (still sometimes called by its olde name "Secure Sockets Layer", or SSL) was created to enable HTTPS, allow - internet users to log into web sites securely, and purchase things online by entering their credit card number. + TLS, (still sometimes called by its olde name "Secure Sockets Layer", or SSL) was created to enable HTTPS, to allow + internet users to log into web sites securely and purchase things online by entering their credit card number. Of course, this required security that actually works; if someone could MITM attack the connection, they could easily steal tons of credit card numbers and passwords.

    From cee23f1799afcd9a7a0ee70053b79ebf4796203f Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 01:24:46 -0600 Subject: [PATCH 34/41] apostrophe --- capsulflask/templates/about-ssh.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index 619e482..a46d547 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -155,7 +155,7 @@ and guaranteeing authenticity was in the CA's business interest. The CAs kept their private keys behind razor wire and armed guards, and followed strict rules to ensure that only the right people got thier certificates signed. - Only the CA's themselves or anyone who had enough power to force them to create a fraudulent certificate + Only the CAs themselves or anyone who had enough power to force them to create a fraudulent certificate would be able to execute MITM attacks.

    From d878a07350e57cd5fd988dc34df9474e112fc6de Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 01:31:20 -0600 Subject: [PATCH 35/41] Will anyone actually attack you like that? Who knows. Personally, I'd rather not find out. --- capsulflask/templates/about-ssh.html | 1 + 1 file changed, 1 insertion(+) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index a46d547..35e66e0 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -258,6 +258,7 @@ Host key verification failed. completely disabling all security of the SSH connection. It can be fully man-in-the-middle attacked & you are vulnerable to surveillance, command injection, even emulation/falsification of the entire stream. + Will anyone actually attack you like that? Who knows. Personally, I'd rather not find out.

    From a853eeef69bd892e0fe4bdc7c0f8df778023b2f7 Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 02:09:42 -0600 Subject: [PATCH 36/41] replace nonsensical namecoin plug with "Why ssh more ssh" --- capsulflask/templates/about-ssh.html | 50 ++++++++++++---------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index 35e66e0..6c323d6 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -301,38 +301,32 @@ Host key verification failed. with confidence that they are not being MITM attacked.

    -

    It's 2021. Can't we do better than this? What's next?

    +

    Why ssh more ssh

    - Glad you asked 😜. + SSH is a relatively low-level protocol, it should be kept simple and it should not depend on anything external. + It has to be this way, because often times SSH is the first service that runs on a server, before any other + services or processes launch. SSH server has to run no matter what, because it's what we're gonna depend on to + log in there and fix everything else which is broken! Also, SSH has to work for all computers, not just the ones which + are reachable publically. So, arguing that SSH should be wrapped in TLS or that SSH should use x.509 doesn't make much sense. +

    +
    +

    + > ssh didn’t needed an upgrade. SSH is perfect +

    +
    +

    + Because of the case for absolute simplicity, I think if anything, + it might even make sense to remove the TOFU and make ssh even less user friendly; requiring the + expected host key to be passed in on every command would dramatically increase the security of real-world SSH usage. + This might already be possible with SSH client configuration. + In order to make it more human-friendly again while keeping the security benefits, + we can create a new layer of abstraction on top of SSH, create regime-specific automation & wrapper scripts.

    - TLS is great, except it has one problem: the X.509 CA system centralizes power and structurally invites abuse. - Power corrupts, and absolute power corrupts absolutely. But there is hope for the future: with the invention of Bitcoin - in 2009, we now have a new tool to use for authority-free secure consensus. Some bright folks have forked Bitcoin to produce - Namecoin, a DNS-like public blockchain which is - merge-mined with Bitcoin, and which allows users to - - register and trade names, including domain names. - In fact, Namecoin features a - - specification for associating public keys with domain names - - and easy-to-use client software packages capable of resolving these - names - & - - public - - - keys, - capable of replacing both the DNS system and X.509 Certificate Authority system. -

    - -

    - For more information on how to get started with Namecoin, see my - - Namecoin guide for webmasters. + For example, when we build a JSON API for capsul, we could also provide a capsul-cli + application which contains an SSH wrapper that knows how to automatically grab & inject the authentic host keys and invoke ssh + in a single command.

    From 9222daefbc41542854fc96f78a0ee98050ada3de Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 02:13:55 -0600 Subject: [PATCH 37/41] fix wording --- capsulflask/templates/about-ssh.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index 6c323d6..82b9a4d 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -317,9 +317,9 @@ Host key verification failed.


    Because of the case for absolute simplicity, I think if anything, - it might even make sense to remove the TOFU and make ssh even less user friendly; requiring the + it might even make sense to remove the TOFU and make the ssh client even less user friendly; requiring the expected host key to be passed in on every command would dramatically increase the security of real-world SSH usage. - This might already be possible with SSH client configuration. + This might already be possible with a custom SSH client configuration. In order to make it more human-friendly again while keeping the security benefits, we can create a new layer of abstraction on top of SSH, create regime-specific automation & wrapper scripts.

    From 756869992055e4ae5e52f90efe7e86f6a4f014ac Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 02:16:13 -0600 Subject: [PATCH 38/41] clearly specify that its about the default ssh config --- capsulflask/templates/about-ssh.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index 82b9a4d..b6e5509 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -318,8 +318,8 @@ Host key verification failed.

    Because of the case for absolute simplicity, I think if anything, it might even make sense to remove the TOFU and make the ssh client even less user friendly; requiring the - expected host key to be passed in on every command would dramatically increase the security of real-world SSH usage. - This might already be possible with a custom SSH client configuration. + expected host key to be passed in on every command by default + would dramatically increase the security of real-world SSH usage. In order to make it more human-friendly again while keeping the security benefits, we can create a new layer of abstraction on top of SSH, create regime-specific automation & wrapper scripts.

    From 51721ffdc0d87729f6a8b9e5e8c06cb2f06fe105 Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 02:20:20 -0600 Subject: [PATCH 39/41] wording --- capsulflask/templates/about-ssh.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index b6e5509..5a9ec65 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -308,7 +308,8 @@ Host key verification failed. It has to be this way, because often times SSH is the first service that runs on a server, before any other services or processes launch. SSH server has to run no matter what, because it's what we're gonna depend on to log in there and fix everything else which is broken! Also, SSH has to work for all computers, not just the ones which - are reachable publically. So, arguing that SSH should be wrapped in TLS or that SSH should use x.509 doesn't make much sense. + have internet access or are reachable publically. + So, arguing that SSH should be wrapped in TLS or that SSH should use x.509 doesn't make much sense.


    From cbe658b7ae321dbb265734cb1139d93ca7adaaec Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 02:31:27 -0600 Subject: [PATCH 40/41] clarify cloud based use case for hypothetical non-tofu ssh --- capsulflask/templates/about-ssh.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capsulflask/templates/about-ssh.html b/capsulflask/templates/about-ssh.html index 5a9ec65..7247c3a 100644 --- a/capsulflask/templates/about-ssh.html +++ b/capsulflask/templates/about-ssh.html @@ -317,7 +317,7 @@ Host key verification failed.


    - Because of the case for absolute simplicity, I think if anything, + Because of the case for absolute simplicity, I think that in a cloud based use-case it might even make sense to remove the TOFU and make the ssh client even less user friendly; requiring the expected host key to be passed in on every command by default would dramatically increase the security of real-world SSH usage. From 645ddede02500af71245d7e1a148b5c8df7ad203 Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 31 Jan 2021 18:02:46 -0600 Subject: [PATCH 41/41] fix a bug where accumulating stale login tokens can lock out an account --- capsulflask/db_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capsulflask/db_model.py b/capsulflask/db_model.py index 82638f5..bd7ec57 100644 --- a/capsulflask/db_model.py +++ b/capsulflask/db_model.py @@ -21,7 +21,7 @@ class DBModel: if hasExactMatch == 0: self.cursor.execute("INSERT INTO accounts (email, lower_case_email) VALUES (%s, %s)", (email, email.lower())) - self.cursor.execute("SELECT token FROM login_tokens WHERE email = %s", (email, )) + self.cursor.execute("SELECT token FROM login_tokens WHERE email = %s and created > (NOW() - INTERVAL '20 min')", (email, )) if len(self.cursor.fetchall()) > 2: return (None, ignoreCaseMatches)