From ac86b2043dea8f15cb0670db98c2bf21e1843d4b Mon Sep 17 00:00:00 2001 From: Pablo Carranza Date: Tue, 1 Mar 2016 17:53:01 +0000 Subject: [PATCH 01/11] Backport authorized_keys branch 'find-key-by-fingerprint' Add find key by base64 key or fingerprint to the internal API See merge request !250 Squashed changes: Add unique index to fingerprint Add new index to schema Add internal api to get ssh key by fingerprint Change API endpoint to authorized_keys Add InsecureKeyFingerprint that calculates the fingerprint without shelling out Add require for gitlab key fingerprint Remove uniqueness of fingerprint index Remove unique option from migration Fix spec style in fingerprint test Fix rubocop complain Extract insecure key fingerprint to separate file Change migration to support building index concurrently Remove those hideous tabs --- .../20160301174731_add_fingerprint_index.rb | 16 +++++++ lib/api/internal.rb | 12 +++++ lib/gitlab/insecure_key_fingerprint.rb | 23 +++++++++ .../gitlab/insecure_key_fingerprint_spec.rb | 18 +++++++ spec/requests/api/internal_spec.rb | 48 +++++++++++++++++++ 5 files changed, 117 insertions(+) create mode 100644 db/migrate/20160301174731_add_fingerprint_index.rb create mode 100644 lib/gitlab/insecure_key_fingerprint.rb create mode 100644 spec/lib/gitlab/insecure_key_fingerprint_spec.rb diff --git a/db/migrate/20160301174731_add_fingerprint_index.rb b/db/migrate/20160301174731_add_fingerprint_index.rb new file mode 100644 index 00000000000..b7c4f7d140a --- /dev/null +++ b/db/migrate/20160301174731_add_fingerprint_index.rb @@ -0,0 +1,16 @@ +# rubocop:disable all +class AddFingerprintIndex < ActiveRecord::Migration + disable_ddl_transaction! + + DOWNTIME = false + + def change + args = [:keys, :fingerprint] + + if Gitlab::Database.postgresql? + args << { algorithm: :concurrently } + end + + add_index(*args) + end +end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 79b302aae70..8bf53939751 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -81,6 +81,18 @@ module API merge_request_urls end + # + # Get a ssh key using the fingerprint + # + get "/authorized_keys" do + fingerprint = params.fetch(:fingerprint) do + Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint + end + key = Key.find_by(fingerprint: fingerprint) + not_found!("Key") if key.nil? + present key, with: Entities::SSHKey + end + # # Discover user by ssh key or user id # diff --git a/lib/gitlab/insecure_key_fingerprint.rb b/lib/gitlab/insecure_key_fingerprint.rb new file mode 100644 index 00000000000..f85b6e9197f --- /dev/null +++ b/lib/gitlab/insecure_key_fingerprint.rb @@ -0,0 +1,23 @@ +module Gitlab + # + # Calculates the fingerprint of a given key without using + # openssh key validations. For this reason, only use + # for calculating the fingerprint to find the key with it. + # + # DO NOT use it for checking the validity of a ssh key. + # + class InsecureKeyFingerprint + attr_accessor :key + + # + # Gets the base64 encoded string representing a rsa or dsa key + # + def initialize(key_base64) + @key = key_base64 + end + + def fingerprint + OpenSSL::Digest::MD5.hexdigest(Base64.decode64(@key)).scan(/../).join(':') + end + end +end diff --git a/spec/lib/gitlab/insecure_key_fingerprint_spec.rb b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb new file mode 100644 index 00000000000..6532579b1c9 --- /dev/null +++ b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Gitlab::InsecureKeyFingerprint do + let(:key) do + 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn' \ + '1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qk' \ + 'r8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMg' \ + 'Jw0=' + end + + let(:fingerprint) { "3f:a2:ee:de:b5:de:53:c3:aa:2f:9c:45:24:4c:47:7b" } + + describe "#fingerprint" do + it "generates the key's fingerprint" do + expect(described_class.new(key.split[1]).fingerprint).to eq(fingerprint) + end + end +end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 7b25047ea8f..7b5fddde456 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -192,6 +192,54 @@ describe API::Internal do end end + describe "GET /internal/authorized_keys" do + context "unsing an existing key's fingerprint" do + it "finds the key" do + get(api('/internal/authorized_keys'), fingerprint: key.fingerprint, secret_token: secret_token) + + expect(response.status).to eq(200) + expect(json_response["key"]).to eq(key.key) + end + end + + context "non existing key's fingerprint" do + it "returns 404" do + get(api('/internal/authorized_keys'), fingerprint: "no:t-:va:li:d0", secret_token: secret_token) + + expect(response.status).to eq(404) + end + end + + context "using a partial fingerprint" do + it "returns 404" do + get(api('/internal/authorized_keys'), fingerprint: "#{key.fingerprint[0..5]}%", secret_token: secret_token) + + expect(response.status).to eq(404) + end + end + + context "sending the key" do + it "finds the key" do + get(api('/internal/authorized_keys'), key: key.key.split[1], secret_token: secret_token) + + expect(response.status).to eq(200) + expect(json_response["key"]).to eq(key.key) + end + + it "returns 404 with a partial key" do + get(api('/internal/authorized_keys'), key: key.key.split[1][0...-3], secret_token: secret_token) + + expect(response.status).to eq(404) + end + + it "returns 404 with an not valid base64 string" do + get(api('/internal/authorized_keys'), key: "whatever!", secret_token: secret_token) + + expect(response.status).to eq(404) + end + end + end + describe "POST /internal/allowed", :clean_gitlab_redis_shared_state do context "access granted" do around do |example| From 255a0f85e3b62845b58f5a4aa189e57f36992c77 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Tue, 30 May 2017 16:24:45 -0700 Subject: [PATCH 02/11] Backport option to disable writing to `authorized_keys` file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Originally branch 'mk-toggle-writing-to-auth-keys-1631' See merge request !2004 Squashed commits: Add authorized_keys_enabled to Application Settings Ensure default settings are exposed in UI Without this change, `authorized_keys_enabled` is unchecked when it is nil, even if it should be checked by default. Add “Speed up SSH operations” documentation Clarify the reasons for disabling writes Add "How to go back" section Tweak copy Update Application Setting screenshot --- .../admin/application_settings_controller.rb | 2 +- app/helpers/application_settings_helper.rb | 1 + app/models/application_setting.rb | 1 + .../application_settings/_form.html.haml | 16 +++ ...ed_keys_enabled_to_application_settings.rb | 15 +++ db/schema.rb | 1 + .../img/write_to_authorized_keys_setting.png | Bin 0 -> 94218 bytes doc/administration/operations/index.md | 3 +- doc/administration/operations/speed_up_ssh.md | 69 +++++++++++ lib/gitlab/shell.rb | 12 ++ spec/lib/gitlab/shell_spec.rb | 110 ++++++++++++++++-- 11 files changed, 217 insertions(+), 13 deletions(-) create mode 100644 db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb create mode 100644 doc/administration/operations/img/write_to_authorized_keys_setting.png create mode 100644 doc/administration/operations/speed_up_ssh.md diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 4dfb397e82c..4de808eb71f 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -52,7 +52,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController private def set_application_setting - @application_setting = ApplicationSetting.current + @application_setting = current_application_settings end def application_setting_params diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index b12ea760668..45f7d29eb05 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -146,6 +146,7 @@ module ApplicationSettingsHelper :after_sign_up_text, :akismet_api_key, :akismet_enabled, + :authorized_keys_enabled, :auto_devops_enabled, :circuitbreaker_access_retries, :circuitbreaker_check_interval, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 253e213af81..8ab338d873d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -261,6 +261,7 @@ class ApplicationSetting < ActiveRecord::Base { after_sign_up_text: nil, akismet_enabled: false, + authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand container_registry_token_expire_delay: 5, default_artifacts_expire_in: '30 days', default_branch_protection: Settings.gitlab['default_branch_protection'], diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 3e2dbb07a6c..ba4ca88a8a9 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -774,6 +774,22 @@ installations. Set to 0 to completely disable polling. = link_to icon('question-circle'), help_page_path('administration/polling') + %fieldset + %legend Performance optimization + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :authorized_keys_enabled do + = f.check_box :authorized_keys_enabled + Write to "authorized_keys" file + .help-block + By default, we write to the "authorized_keys" file to support Git + over SSH without additional configuration. GitLab can be optimized + to authenticate SSH keys via the database file. Only uncheck this + if you have configured your OpenSSH server to use the + AuthorizedKeysCommand. Click on the help icon for more details. + = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup') + %fieldset %legend User and IP Rate Limits .form-group diff --git a/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb b/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb new file mode 100644 index 00000000000..fdae309946c --- /dev/null +++ b/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb @@ -0,0 +1,15 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddAuthorizedKeysEnabledToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + # allow_null: true because we want to set the default based on if the + # instance is configured to use AuthorizedKeysCommand + add_column :application_settings, :authorized_keys_enabled, :boolean, allow_null: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 740e80ccfd4..11273d2a82e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -154,6 +154,7 @@ ActiveRecord::Schema.define(version: 20171230123729) do t.integer "gitaly_timeout_default", default: 55, null: false t.integer "gitaly_timeout_medium", default: 30, null: false t.integer "gitaly_timeout_fast", default: 10, null: false + t.boolean "authorized_keys_enabled" end create_table "audit_events", force: :cascade do |t| diff --git a/doc/administration/operations/img/write_to_authorized_keys_setting.png b/doc/administration/operations/img/write_to_authorized_keys_setting.png new file mode 100644 index 0000000000000000000000000000000000000000..232765f1917960eb3858ed8dd3ab939ebc757e96 GIT binary patch literal 94218 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

#>dY5X1RRZfg)Vd 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�i&?$EENv6Xp0&0FUz-+ccT~?QajIQzo0Z!l%jTx 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 literal 0 HcmV?d00001 diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md index 320d71a9527..f96a084c853 100644 --- a/doc/administration/operations/index.md +++ b/doc/administration/operations/index.md @@ -13,4 +13,5 @@ by GitLab to another file system or another server. that to prioritize important jobs. - [Sidekiq MemoryKiller](sidekiq_memory_killer.md): Configure Sidekiq MemoryKiller to restart Sidekiq. -- [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer. \ No newline at end of file +- [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer. +- [Speed up SSH operations](speed_up_ssh.md) diff --git a/doc/administration/operations/speed_up_ssh.md b/doc/administration/operations/speed_up_ssh.md new file mode 100644 index 00000000000..9b260beb34f --- /dev/null +++ b/doc/administration/operations/speed_up_ssh.md @@ -0,0 +1,69 @@ +# Speed up SSH operations + +## The problem + +SSH operations become slow as the number of users grows. + +## The reason + +OpenSSH searches for a key to authorize a user via a linear search. In the worst case, such as when the user is not authorized to access GitLab, OpenSSH will scan the entire file to search for a key. This can take significant time and disk I/O, which will delay users attempting to push or pull to a repository. Making matters worse, if users add or remove keys frequently, the operating system may not be able to cache the authorized_keys file, which causes the disk to be accessed repeatedly. + +## The solution + +GitLab Shell provides a way to authorize SSH users via a fast, indexed lookup to the GitLab database. GitLab Shell uses the fingerprint of the SSH key to check whether the user is authorized to access GitLab. + +> **Warning:** OpenSSH version 6.9+ is required because `AuthorizedKeysCommand` must be able to accept a fingerprint. These instructions will break installations using older versions of OpenSSH, such as those included with CentOS as of May 2017. + +Create this file at `/opt/gitlab-shell/authorized_keys`: + +``` +#!/bin/bash + +if [[ "$1" == "git" ]]; then + /opt/gitlab/embedded/service/gitlab-shell/bin/authorized_keys $2 +fi +``` + +Set appropriate ownership and permissions: + +``` +sudo chown root:git /opt/gitlab-shell/authorized_keys +sudo chmod 0650 /opt/gitlab-shell/authorized_keys +``` + +Add the following to `/etc/ssh/sshd_config`: + +``` +AuthorizedKeysCommand /opt/gitlab-shell/authorized_keys %u %k +AuthorizedKeysCommandUser git +``` + +Reload the sshd service: + +``` +sudo service sshd reload +``` + +Confirm that SSH is working by removing your user's SSH key in the UI, adding a new one, and attempting to pull a repo. + +> **Warning:** Do not disable writes until SSH is confirmed to be working perfectly because the file will quickly become out-of-date. + +In the case of lookup failures (which are not uncommon), the `authorized_keys` file will still be scanned. So git SSH performance will still be slow for many users as long as a large file exists. + +You can disable any more writes to the `authorized_keys` file by unchecking `Write to "authorized_keys" file` in the Application Settings of your GitLab installation. + +![Write to authorized keys setting](img/write_to_authorized_keys_setting.png) + +Again, confirm that SSH is working by removing your user's SSH key in the UI, adding a new one, and attempting to pull a repo. + +Then you can backup and delete your `authorized_keys` file for best performance. + +## How to go back to using the `authorized_keys` file + +This is a brief overview. Please refer to the above instructions for more context. + +1. Rebuild the `authorized_keys` file. See https://docs.gitlab.com/ce/administration/raketasks/maintenance.html#rebuild-authorized_keys-file +1. Enable writes to the `authorized_keys` file +1. Remove the `AuthorizedKeysCommand` lines from `/etc/ssh/sshd_config` +1. Reload the sshd service +1. Remove the `/opt/gitlab-shell/authorized_keys` file diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 564047bbd34..20df19c734d 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -183,6 +183,8 @@ module Gitlab # add_key("key-42", "sha-rsa ...") # def add_key(key_id, key_content) + return unless self.authorized_keys_enabled? + gitlab_shell_fast_execute([gitlab_shell_keys_path, 'add-key', key_id, self.class.strip_key(key_content)]) end @@ -192,6 +194,8 @@ module Gitlab # Ex. # batch_add_keys { |adder| adder.add_key("key-42", "sha-rsa ...") } def batch_add_keys(&block) + return unless self.authorized_keys_enabled? + IO.popen(%W(#{gitlab_shell_path}/bin/gitlab-keys batch-add-keys), 'w') do |io| yield(KeyAdder.new(io)) end @@ -203,6 +207,8 @@ module Gitlab # remove_key("key-342", "sha-rsa ...") # def remove_key(key_id, key_content) + return unless self.authorized_keys_enabled? + args = [gitlab_shell_keys_path, 'rm-key', key_id] args << key_content if key_content @@ -215,6 +221,8 @@ module Gitlab # remove_all_keys # def remove_all_keys + return unless self.authorized_keys_enabled? + gitlab_shell_fast_execute([gitlab_shell_keys_path, 'clear']) end @@ -410,5 +418,9 @@ module Gitlab # need to do the same here... raise Error, e end + + def authorized_keys_enabled? + current_application_settings.authorized_keys_enabled + end end end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 81d9e6a8f82..6d50f537430 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -51,6 +51,105 @@ describe Gitlab::Shell do end end + describe '#add_key' do + context 'when authorized_keys_enabled is true' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(Gitlab::Utils).to receive(:system_silent).with( + [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect(Gitlab::Utils).not_to receive(:system_silent) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + end + + describe '#batch_add_keys' do + context 'when authorized_keys_enabled is true' do + it 'instantiates KeyAdder' do + expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + + gitlab_shell.batch_add_keys do |adder| + adder.add_key('key-123', 'ssh-rsa foobar') + end + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect_any_instance_of(Gitlab::Shell::KeyAdder).not_to receive(:add_key) + + gitlab_shell.batch_add_keys do |adder| + adder.add_key('key-123', 'ssh-rsa foobar') + end + end + end + end + + describe '#remove_key' do + context 'when authorized_keys_enabled is true' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(Gitlab::Utils).to receive(:system_silent).with( + [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect(Gitlab::Utils).not_to receive(:system_silent) + + gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + end + end + end + + describe '#remove_all_keys' do + context 'when authorized_keys_enabled is true' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(Gitlab::Utils).to receive(:system_silent).with([:gitlab_shell_keys_path, 'clear']) + + gitlab_shell.remove_all_keys + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect(Gitlab::Utils).not_to receive(:system_silent) + + gitlab_shell.remove_all_keys + end + end + end + describe Gitlab::Shell::KeyAdder do describe '#add_key' do it 'removes trailing garbage' do @@ -96,17 +195,6 @@ describe Gitlab::Shell do allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) end - describe '#add_key' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end - end - describe '#add_repository' do shared_examples '#add_repository' do let(:repository_storage) { 'default' } From 07bd79cd721a13fc012b77b09e56cd709e5a22f3 Mon Sep 17 00:00:00 2001 From: Ernst van Nierop Date: Fri, 15 Dec 2017 12:39:44 +0000 Subject: [PATCH 03/11] Combine ssh docs and rename the doc Backport to CE, originally branch 'evn-ssh-clarify-docs See merge request gitlab-org/gitlab-ee!3753 --- .../operations/fast_ssh_key_lookup.md | 192 ++++++++++++++++++ doc/administration/operations/index.md | 2 +- doc/administration/operations/speed_up_ssh.md | 70 +------ 3 files changed, 194 insertions(+), 70 deletions(-) create mode 100644 doc/administration/operations/fast_ssh_key_lookup.md diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md new file mode 100644 index 00000000000..b86168f935a --- /dev/null +++ b/doc/administration/operations/fast_ssh_key_lookup.md @@ -0,0 +1,192 @@ +# Fast lookup of authorized SSH keys in the database + +Regular SSH operations become slow as the number of users grows because OpenSSH +searches for a key to authorize a user via a linear search. In the worst case, +such as when the user is not authorized to access GitLab, OpenSSH will scan the +entire file to search for a key. This can take significant time and disk I/O, +which will delay users attempting to push or pull to a repository. Making +matters worse, if users add or remove keys frequently, the operating system may +not be able to cache the `authorized_keys` file, which causes the disk to be +accessed repeatedly. + +GitLab Shell solves this by providing a way to authorize SSH users via a fast, +indexed lookup in the GitLab database. This page describes how to enable the fast +lookup of authorized SSH keys. + +> **Warning:** OpenSSH version 6.9+ is required because +`AuthorizedKeysCommand` must be able to accept a fingerprint. These +instructions will break installations using older versions of OpenSSH, such as +those included with CentOS 6 as of September 2017. If you want to use this +feature for CentOS 6, follow [the instructions on how to build and install a custom OpenSSH package](#compiling-a-custom-version-of-openssh-for-centos-6) before continuing. + +## Setting up fast lookup via GitLab Shell + +GitLab Shell provides a way to authorize SSH users via a fast, indexed lookup +to the GitLab database. GitLab Shell uses the fingerprint of the SSH key to +check whether the user is authorized to access GitLab. + +Create the directory `/opt/gitlab-shell` first: + +```bash +sudo mkdir -p /opt/gitlab-shell +``` + +Create this file at `/opt/gitlab-shell/authorized_keys`: + +``` +#!/bin/bash + +if [[ "$1" == "git" ]]; then + /opt/gitlab/embedded/service/gitlab-shell/bin/authorized_keys $2 +fi +``` + +Set appropriate ownership and permissions: + +``` +sudo chown root:git /opt/gitlab-shell/authorized_keys +sudo chmod 0650 /opt/gitlab-shell/authorized_keys +``` + +Add the following to `/etc/ssh/sshd_config` or to `/assets/sshd_config` if you +are using Omnibus Docker: + +``` +AuthorizedKeysCommand /opt/gitlab-shell/authorized_keys %u %k +AuthorizedKeysCommandUser git +``` + +Reload OpenSSH: + +```bash +# Debian or Ubuntu installations +sudo service ssh reload + +# CentOS installations +sudo service sshd reload +``` + +Confirm that SSH is working by removing your user's SSH key in the UI, adding a +new one, and attempting to pull a repo. + +> **Warning:** Do not disable writes until SSH is confirmed to be working +perfectly because the file will quickly become out-of-date. + +In the case of lookup failures (which are not uncommon), the `authorized_keys` +file will still be scanned. So git SSH performance will still be slow for many +users as long as a large file exists. + +You can disable any more writes to the `authorized_keys` file by unchecking +`Write to "authorized_keys" file` in the Application Settings of your GitLab +installation. + +![Write to authorized keys setting](img/write_to_authorized_keys_setting.png) + +Again, confirm that SSH is working by removing your user's SSH key in the UI, +adding a new one, and attempting to pull a repo. + +Then you can backup and delete your `authorized_keys` file for best performance. + +## How to go back to using the `authorized_keys` file + +This is a brief overview. Please refer to the above instructions for more context. + +1. [Rebuild the `authorized_keys` file](../raketasks/maintenance.md#rebuild-authorized_keys-file) +1. Enable writes to the `authorized_keys` file in Application Settings +1. Remove the `AuthorizedKeysCommand` lines from `/etc/ssh/sshd_config` or from `/assets/sshd_config` if you are using Omnibus Docker. +1. Reload sshd: `sudo service sshd reload` +1. Remove the `/opt/gitlab-shell/authorized_keys` file + +## Compiling a custom version of OpenSSH for CentOS 6 + +Building a custom version of OpenSSH is not necessary for Ubuntu 16.04 users, +since Ubuntu 16.04 ships with OpenSSH 7.2. + +It is also unnecessary for CentOS 7.4 users, as that version ships with +OpenSSH 7.4. If you are using CentOS 7.0 - 7.3, we strongly recommend that you +upgrade to CentOS 7.4 instead of following this procedure. This should be as +simple as running `yum update`. + +CentOS 6 users must build their own OpenSSH package to enable SSH lookups via +the database. The following instructions can be used to build OpenSSH 7.5: + +1. First, download the package and install the required packages: + + ``` + sudo su - + cd /tmp + curl --remote-name https://mirrors.evowise.com/pub/OpenBSD/OpenSSH/portable/openssh-7.5p1.tar.gz + tar xzvf openssh-7.5p1.tar.gz + yum install rpm-build gcc make wget openssl-devel krb5-devel pam-devel libX11-devel xmkmf libXt-devel + ``` + +3. Prepare the build by copying files to the right place: + + ``` + mkdir -p /root/rpmbuild/{SOURCES,SPECS} + cp ./openssh-7.5p1/contrib/redhat/openssh.spec /root/rpmbuild/SPECS/ + cp openssh-7.5p1.tar.gz /root/rpmbuild/SOURCES/ + cd /root/rpmbuild/SPECS + ``` + +3. Next, set the spec settings properly: + + ``` + sed -i -e "s/%define no_gnome_askpass 0/%define no_gnome_askpass 1/g" openssh.spec + sed -i -e "s/%define no_x11_askpass 0/%define no_x11_askpass 1/g" openssh.spec + sed -i -e "s/BuildPreReq/BuildRequires/g" openssh.spec + ``` + +3. Build the RPMs: + + ``` + rpmbuild -bb openssh.spec + ``` + +4. Ensure the RPMs were built: + + ``` + ls -al /root/rpmbuild/RPMS/x86_64/ + ``` + + You should see something as the following: + + ``` + total 1324 + drwxr-xr-x. 2 root root 4096 Jun 20 19:37 . + drwxr-xr-x. 3 root root 19 Jun 20 19:37 .. + -rw-r--r--. 1 root root 470828 Jun 20 19:37 openssh-7.5p1-1.x86_64.rpm + -rw-r--r--. 1 root root 490716 Jun 20 19:37 openssh-clients-7.5p1-1.x86_64.rpm + -rw-r--r--. 1 root root 17020 Jun 20 19:37 openssh-debuginfo-7.5p1-1.x86_64.rpm + -rw-r--r--. 1 root root 367516 Jun 20 19:37 openssh-server-7.5p1-1.x86_64.rpm + ``` + +5. Install the packages. OpenSSH packages will replace `/etc/pam.d/sshd` + with its own version, which may prevent users from logging in, so be sure + that the file is backed up and restored after installation: + + ``` + timestamp=$(date +%s) + cp /etc/pam.d/sshd pam-ssh-conf-$timestamp + rpm -Uvh /root/rpmbuild/RPMS/x86_64/*.rpm + yes | cp pam-ssh-conf-$timestamp /etc/pam.d/sshd + ``` + +6. Verify the installed version. In another window, attempt to login to the server: + + ``` + ssh -v + ``` + + You should see a line that reads: "debug1: Remote protocol version 2.0, remote software version OpenSSH_7.5" + + If not, you may need to restart sshd (e.g. `systemctl restart sshd.service`). + +7. *IMPORTANT!* Open a new SSH session to your server before exiting to make + sure everything is working! If you need to downgrade, simple install the + older package: + + ``` + # Only run this if you run into a problem logging in + yum downgrade openssh-server openssh openssh-clients + ``` diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md index f96a084c853..5655b7efec6 100644 --- a/doc/administration/operations/index.md +++ b/doc/administration/operations/index.md @@ -14,4 +14,4 @@ that to prioritize important jobs. - [Sidekiq MemoryKiller](sidekiq_memory_killer.md): Configure Sidekiq MemoryKiller to restart Sidekiq. - [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer. -- [Speed up SSH operations](speed_up_ssh.md) +- [Speed up SSH operations](fast_ssh_key_lookup.md): Authorize SSH users via a fast, indexed lookup to the GitLab database. diff --git a/doc/administration/operations/speed_up_ssh.md b/doc/administration/operations/speed_up_ssh.md index 9b260beb34f..89265b3018b 100644 --- a/doc/administration/operations/speed_up_ssh.md +++ b/doc/administration/operations/speed_up_ssh.md @@ -1,69 +1 @@ -# Speed up SSH operations - -## The problem - -SSH operations become slow as the number of users grows. - -## The reason - -OpenSSH searches for a key to authorize a user via a linear search. In the worst case, such as when the user is not authorized to access GitLab, OpenSSH will scan the entire file to search for a key. This can take significant time and disk I/O, which will delay users attempting to push or pull to a repository. Making matters worse, if users add or remove keys frequently, the operating system may not be able to cache the authorized_keys file, which causes the disk to be accessed repeatedly. - -## The solution - -GitLab Shell provides a way to authorize SSH users via a fast, indexed lookup to the GitLab database. GitLab Shell uses the fingerprint of the SSH key to check whether the user is authorized to access GitLab. - -> **Warning:** OpenSSH version 6.9+ is required because `AuthorizedKeysCommand` must be able to accept a fingerprint. These instructions will break installations using older versions of OpenSSH, such as those included with CentOS as of May 2017. - -Create this file at `/opt/gitlab-shell/authorized_keys`: - -``` -#!/bin/bash - -if [[ "$1" == "git" ]]; then - /opt/gitlab/embedded/service/gitlab-shell/bin/authorized_keys $2 -fi -``` - -Set appropriate ownership and permissions: - -``` -sudo chown root:git /opt/gitlab-shell/authorized_keys -sudo chmod 0650 /opt/gitlab-shell/authorized_keys -``` - -Add the following to `/etc/ssh/sshd_config`: - -``` -AuthorizedKeysCommand /opt/gitlab-shell/authorized_keys %u %k -AuthorizedKeysCommandUser git -``` - -Reload the sshd service: - -``` -sudo service sshd reload -``` - -Confirm that SSH is working by removing your user's SSH key in the UI, adding a new one, and attempting to pull a repo. - -> **Warning:** Do not disable writes until SSH is confirmed to be working perfectly because the file will quickly become out-of-date. - -In the case of lookup failures (which are not uncommon), the `authorized_keys` file will still be scanned. So git SSH performance will still be slow for many users as long as a large file exists. - -You can disable any more writes to the `authorized_keys` file by unchecking `Write to "authorized_keys" file` in the Application Settings of your GitLab installation. - -![Write to authorized keys setting](img/write_to_authorized_keys_setting.png) - -Again, confirm that SSH is working by removing your user's SSH key in the UI, adding a new one, and attempting to pull a repo. - -Then you can backup and delete your `authorized_keys` file for best performance. - -## How to go back to using the `authorized_keys` file - -This is a brief overview. Please refer to the above instructions for more context. - -1. Rebuild the `authorized_keys` file. See https://docs.gitlab.com/ce/administration/raketasks/maintenance.html#rebuild-authorized_keys-file -1. Enable writes to the `authorized_keys` file -1. Remove the `AuthorizedKeysCommand` lines from `/etc/ssh/sshd_config` -1. Reload the sshd service -1. Remove the `/opt/gitlab-shell/authorized_keys` file +This document was moved to [another location](fast_ssh_key_lookup.md). From bcffeade9df825d40a4fe9cf9c1401b326c1fa9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 11 Oct 2017 16:45:56 +0200 Subject: [PATCH 04/11] Use ApplicationSetting.current in Admin::ApplicationSettingsController See merge request gitlab-org/gitlab-ee!3323 Backported as part of authorized_keys in https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/16014 --- app/controllers/admin/application_settings_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 4de808eb71f..4dfb397e82c 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -52,7 +52,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController private def set_application_setting - @application_setting = current_application_settings + @application_setting = ApplicationSetting.current end def application_setting_params From 797fe0a6e6ce2f1241d2bd22c4b491c367e796fd Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Mon, 26 Jun 2017 14:40:08 -0700 Subject: [PATCH 05/11] Backport authorized_keys_enabled defaults to true' Originally from branch 'fix-authorized-keys-enabled-default-2738' via merge request https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2240 Removed background migrations which were intended to fix state after using Gitlab without a default having been set Squashed commits: Locally, if Spring was not restarted, `current_application_settings` was still cached, which prevented the migration from editing the file. This will also ensure that any app server somehow hitting old cache data will properly default this setting regardless. Retroactively fix migration This allows us to identify customers who ran the broken migration. Their `authorized_keys_enabled` column does not have a default at this point. We will fix the column after we fix the `authorized_keys` file. Fix authorized_keys file if needed Add default to authorized_keys_enabled setting Reminder: The original migration was fixed retroactively a few commits ago, so people who did not ever run GitLab 9.3.0 already have a column that defaults to true and disallows nulls. I have tested on PostgreSQL and MySQL that it is safe to run this migration regardless. Affected customers who did run 9.3.0 are the ones who need this migration to fix the authorized_keys_enabled column. The reason for the retroactive fix plus this migration is that it allows us to run a migration in between to fix the authorized_keys file only for those who ran 9.3.0. Tweaks to address feedback Extract work into background migration Move batch-add-logic to background migration Do the work synchronously to avoid multiple workers attempting to add batches of keys at the same time. Also, make the delete portion wait until after adding is done. Do read and delete work in background migration Fix Rubocop offenses Add changelog entry Inform the user of actions taken or not taken Prevent unnecessary `select`s and `remove_key`s Add logs for action taken Fix optimization Reuse `Gitlab::ShellAdapter` Guarantee the earliest key Fix migration spec for MySQL --- ...ed_keys_enabled_to_application_settings.rb | 12 +- db/schema.rb | 2 +- lib/gitlab/shell.rb | 58 ++++- spec/lib/gitlab/shell_spec.rb | 212 ++++++++++++++++++ spec/workers/gitlab_shell_worker_spec.rb | 12 + 5 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 spec/workers/gitlab_shell_worker_spec.rb diff --git a/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb b/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb index fdae309946c..1d86a531eb3 100644 --- a/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb +++ b/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb @@ -7,9 +7,13 @@ class AddAuthorizedKeysEnabledToApplicationSettings < ActiveRecord::Migration # Set this constant to true if this migration requires downtime. DOWNTIME = false - def change - # allow_null: true because we want to set the default based on if the - # instance is configured to use AuthorizedKeysCommand - add_column :application_settings, :authorized_keys_enabled, :boolean, allow_null: true + disable_ddl_transaction! + + def up + add_column_with_default :application_settings, :authorized_keys_enabled, :boolean, default: true, allow_null: false + end + + def down + remove_column :application_settings, :authorized_keys_enabled end end diff --git a/db/schema.rb b/db/schema.rb index 11273d2a82e..840abffd9f4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -154,7 +154,7 @@ ActiveRecord::Schema.define(version: 20171230123729) do t.integer "gitaly_timeout_default", default: 55, null: false t.integer "gitaly_timeout_medium", default: 30, null: false t.integer "gitaly_timeout_fast", default: 10, null: false - t.boolean "authorized_keys_enabled" + t.boolean "authorized_keys_enabled", default: true, null: false end create_table "audit_events", force: :cascade do |t| diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 20df19c734d..65a9e58adf1 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -206,12 +206,11 @@ module Gitlab # Ex. # remove_key("key-342", "sha-rsa ...") # - def remove_key(key_id, key_content) + def remove_key(key_id, key_content = nil) return unless self.authorized_keys_enabled? args = [gitlab_shell_keys_path, 'rm-key', key_id] args << key_content if key_content - gitlab_shell_fast_execute(args) end @@ -226,6 +225,57 @@ module Gitlab gitlab_shell_fast_execute([gitlab_shell_keys_path, 'clear']) end + # Remove ssh keys from gitlab shell that are not in the DB + # + # Ex. + # remove_keys_not_found_in_db + # + def remove_keys_not_found_in_db + return unless self.authorized_keys_enabled? + + Rails.logger.info("Removing keys not found in DB") + + batch_read_key_ids do |ids_in_file| + ids_in_file.uniq! + keys_in_db = Key.where(id: ids_in_file) + + next unless ids_in_file.size > keys_in_db.count # optimization + + ids_to_remove = ids_in_file - keys_in_db.pluck(:id) + ids_to_remove.each do |id| + Rails.logger.info("Removing key-#{id} not found in DB") + remove_key("key-#{id}") + end + end + end + + # Iterate over all ssh key IDs from gitlab shell, in batches + # + # Ex. + # batch_read_key_ids { |batch| keys = Key.where(id: batch) } + # + def batch_read_key_ids(batch_size: 100, &block) + return unless self.authorized_keys_enabled? + + list_key_ids do |key_id_stream| + key_id_stream.lazy.each_slice(batch_size) do |lines| + key_ids = lines.map { |l| l.chomp.to_i } + yield(key_ids) + end + end + end + + # Stream all ssh key IDs from gitlab shell, separated by newlines + # + # Ex. + # list_key_ids + # + def list_key_ids(&block) + return unless self.authorized_keys_enabled? + + IO.popen(%W(#{gitlab_shell_path}/bin/gitlab-keys list-key-ids), &block) + end + # Add empty directory for storing repositories # # Ex. @@ -420,6 +470,10 @@ module Gitlab end def authorized_keys_enabled? + # Return true if nil to ensure the authorized_keys methods work while + # fixing the authorized_keys file during migration. + return true if current_application_settings.authorized_keys_enabled.nil? + current_application_settings.authorized_keys_enabled end end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 6d50f537430..e452fbb757d 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -74,6 +74,21 @@ describe Gitlab::Shell do gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') end end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(Gitlab::Utils).to receive(:system_silent).with( + [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end end describe '#batch_add_keys' do @@ -100,6 +115,20 @@ describe Gitlab::Shell do end end end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'instantiates KeyAdder' do + expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + + gitlab_shell.batch_add_keys do |adder| + adder.add_key('key-123', 'ssh-rsa foobar') + end + end + end end describe '#remove_key' do @@ -125,6 +154,32 @@ describe Gitlab::Shell do gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') end end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(Gitlab::Utils).to receive(:system_silent).with( + [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + end + end + + context 'when key content is not given' do + it 'calls rm-key with only one argument' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(Gitlab::Utils).to receive(:system_silent).with( + [:gitlab_shell_keys_path, 'rm-key', 'key-123'] + ) + + gitlab_shell.remove_key('key-123') + end + end end describe '#remove_all_keys' do @@ -148,6 +203,155 @@ describe Gitlab::Shell do gitlab_shell.remove_all_keys end end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(Gitlab::Utils).to receive(:system_silent).with([:gitlab_shell_keys_path, 'clear']) + + gitlab_shell.remove_all_keys + end + end + end + + describe '#remove_keys_not_found_in_db' do + context 'when keys are in the file that are not in the DB' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') + @another_key = create(:key) # this one IS in the DB + end + + it 'removes the keys' do + expect(find_in_authorized_keys_file(1234)).to be_truthy + expect(find_in_authorized_keys_file(9876)).to be_truthy + expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(1234)).to be_falsey + expect(find_in_authorized_keys_file(9876)).to be_falsey + expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy + end + end + + context 'when keys there are duplicate keys in the file that are not in the DB' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys' do + expect(find_in_authorized_keys_file(1234)).to be_truthy + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(1234)).to be_falsey + end + + it 'does not run remove more than once per key (in a batch)' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234').once + gitlab_shell.remove_keys_not_found_in_db + end + end + + context 'when keys there are duplicate keys in the file that ARE in the DB' do + before do + gitlab_shell.remove_all_keys + @key = create(:key) + gitlab_shell.add_key(@key.shell_id, @key.key) + end + + it 'does not remove the key' do + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(@key.id)).to be_truthy + end + + it 'does not need to run a SELECT query for that batch, on account of that key' do + expect_any_instance_of(ActiveRecord::Relation).not_to receive(:pluck) + gitlab_shell.remove_keys_not_found_in_db + end + end + + unless ENV['CI'] # Skip in CI, it takes 1 minute + context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do + before do + gitlab_shell.remove_all_keys + 100.times { |i| create(:key) } # first batch is all in the DB + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys not in the DB' do + expect(find_in_authorized_keys_file(1234)).to be_truthy + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(1234)).to be_falsey + end + end + end + end + + describe '#batch_read_key_ids' do + context 'when there are keys in the authorized_keys file' do + before do + gitlab_shell.remove_all_keys + (1..4).each do |i| + gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + end + end + + it 'iterates over the key IDs in the file, in batches' do + loop_count = 0 + first_batch = [1, 2] + second_batch = [3, 4] + + gitlab_shell.batch_read_key_ids(batch_size: 2) do |batch| + expected = (loop_count == 0 ? first_batch : second_batch) + expect(batch).to eq(expected) + loop_count += 1 + end + end + end + end + + describe '#list_key_ids' do + context 'when there are keys in the authorized_keys file' do + before do + gitlab_shell.remove_all_keys + (1..4).each do |i| + gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + end + end + + it 'outputs the key IDs in the file, separated by newlines' do + ids = [] + gitlab_shell.list_key_ids do |io| + io.each do |line| + ids << line + end + end + + expect(ids).to eq(%W{1\n 2\n 3\n 4\n}) + end + end + + context 'when there are no keys in the authorized_keys file' do + before do + gitlab_shell.remove_all_keys + end + + it 'outputs nothing, not even an empty string' do + ids = [] + gitlab_shell.list_key_ids do |io| + io.each do |line| + ids << line + end + end + + expect(ids).to eq([]) + end + end end describe Gitlab::Shell::KeyAdder do @@ -484,4 +688,12 @@ describe Gitlab::Shell do end end end + + def find_in_authorized_keys_file(key_id) + gitlab_shell.batch_read_key_ids do |ids| + return true if ids.include?(key_id) + end + + false + end end diff --git a/spec/workers/gitlab_shell_worker_spec.rb b/spec/workers/gitlab_shell_worker_spec.rb new file mode 100644 index 00000000000..6b222af454d --- /dev/null +++ b/spec/workers/gitlab_shell_worker_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe GitlabShellWorker do + let(:worker) { described_class.new } + + describe '#perform with add_key' do + it 'calls add_key on Gitlab::Shell' do + expect_any_instance_of(Gitlab::Shell).to receive(:add_key).with('foo', 'bar') + worker.perform(:add_key, 'foo', 'bar') + end + end +end From d2f4e8f97da171998c2bf4a88765b29528e748b7 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Fri, 8 Jul 2016 07:32:15 +0200 Subject: [PATCH 06/11] Avoid adding index if already exists --- db/migrate/20160301174731_add_fingerprint_index.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db/migrate/20160301174731_add_fingerprint_index.rb b/db/migrate/20160301174731_add_fingerprint_index.rb index b7c4f7d140a..f2c3d1ba1ea 100644 --- a/db/migrate/20160301174731_add_fingerprint_index.rb +++ b/db/migrate/20160301174731_add_fingerprint_index.rb @@ -4,6 +4,7 @@ class AddFingerprintIndex < ActiveRecord::Migration DOWNTIME = false + # https://gitlab.com/gitlab-org/gitlab-ee/issues/764 def change args = [:keys, :fingerprint] @@ -11,6 +12,6 @@ class AddFingerprintIndex < ActiveRecord::Migration args << { algorithm: :concurrently } end - add_index(*args) + add_index(*args) unless index_exists?(:keys, :fingerprint) end end From 01319e5933314616077c6a23cceccdec6ab815af Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 1 Sep 2017 16:20:49 +0100 Subject: [PATCH 07/11] Make Gitlab::CurrentSettings available when getting settings --- lib/gitlab/shell.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 65a9e58adf1..bbe7199c009 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -391,6 +391,14 @@ module Gitlab File.join(gitlab_shell_path, 'bin', 'gitlab-keys') end + def authorized_keys_enabled? + # Return true if nil to ensure the authorized_keys methods work while + # fixing the authorized_keys file during migration. + return true if Gitlab::CurrentSettings.current_application_settings.authorized_keys_enabled.nil? + + Gitlab::CurrentSettings.current_application_settings.authorized_keys_enabled + end + private def gitlab_projects(shard_path, disk_path) @@ -468,13 +476,5 @@ module Gitlab # need to do the same here... raise Error, e end - - def authorized_keys_enabled? - # Return true if nil to ensure the authorized_keys methods work while - # fixing the authorized_keys file during migration. - return true if current_application_settings.authorized_keys_enabled.nil? - - current_application_settings.authorized_keys_enabled - end end end From d9557e43e4eb91eede069c045a962e417ae6e852 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 4 Jul 2017 19:06:18 +0300 Subject: [PATCH 08/11] Backport spec fixes in spec/lib/gitlab/shell_spec.rb --- spec/lib/gitlab/shell_spec.rb | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index e452fbb757d..24fc17861dd 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -55,7 +55,7 @@ describe Gitlab::Shell do context 'when authorized_keys_enabled is true' do it 'removes trailing garbage' do allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(Gitlab::Utils).to receive(:system_silent).with( + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] ) @@ -69,7 +69,7 @@ describe Gitlab::Shell do end it 'does nothing' do - expect(Gitlab::Utils).not_to receive(:system_silent) + expect(Gitlab::Utils).not_to receive(:gitlab_shell_fast_execute) gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') end @@ -82,7 +82,7 @@ describe Gitlab::Shell do it 'removes trailing garbage' do allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(Gitlab::Utils).to receive(:system_silent).with( + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] ) @@ -135,7 +135,7 @@ describe Gitlab::Shell do context 'when authorized_keys_enabled is true' do it 'removes trailing garbage' do allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(Gitlab::Utils).to receive(:system_silent).with( + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] ) @@ -149,7 +149,7 @@ describe Gitlab::Shell do end it 'does nothing' do - expect(Gitlab::Utils).not_to receive(:system_silent) + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') end @@ -162,7 +162,7 @@ describe Gitlab::Shell do it 'removes trailing garbage' do allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(Gitlab::Utils).to receive(:system_silent).with( + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] ) @@ -173,7 +173,7 @@ describe Gitlab::Shell do context 'when key content is not given' do it 'calls rm-key with only one argument' do allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(Gitlab::Utils).to receive(:system_silent).with( + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( [:gitlab_shell_keys_path, 'rm-key', 'key-123'] ) @@ -186,7 +186,7 @@ describe Gitlab::Shell do context 'when authorized_keys_enabled is true' do it 'removes trailing garbage' do allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(Gitlab::Utils).to receive(:system_silent).with([:gitlab_shell_keys_path, 'clear']) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with([:gitlab_shell_keys_path, 'clear']) gitlab_shell.remove_all_keys end @@ -198,7 +198,7 @@ describe Gitlab::Shell do end it 'does nothing' do - expect(Gitlab::Utils).not_to receive(:system_silent) + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) gitlab_shell.remove_all_keys end @@ -211,7 +211,9 @@ describe Gitlab::Shell do it 'removes trailing garbage' do allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(Gitlab::Utils).to receive(:system_silent).with([:gitlab_shell_keys_path, 'clear']) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'clear'] + ) gitlab_shell.remove_all_keys end From 40e3d9f37b31283c5b63ae6ab161ac87e39185d3 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 8 Jan 2018 18:24:51 +0000 Subject: [PATCH 09/11] Fix typo in spec/requests/api/internal_spec.rb --- spec/requests/api/internal_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 7b5fddde456..2783c51b8df 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -193,7 +193,7 @@ describe API::Internal do end describe "GET /internal/authorized_keys" do - context "unsing an existing key's fingerprint" do + context "using an existing key's fingerprint" do it "finds the key" do get(api('/internal/authorized_keys'), fingerprint: key.fingerprint, secret_token: secret_token) From bd9ead683c3bade57a59d06530a1973768622a78 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 8 Jan 2018 19:43:32 +0000 Subject: [PATCH 10/11] Fix spec in shell_spec.rb The spec for "#add_key does nothing" would always have passed, since the expectation was on both the wrong object and message. --- spec/lib/gitlab/shell_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 24fc17861dd..eb90f53468f 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -69,7 +69,7 @@ describe Gitlab::Shell do end it 'does nothing' do - expect(Gitlab::Utils).not_to receive(:gitlab_shell_fast_execute) + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') end From 9edd9a5ea21a1b95ffc7b399c5c11bc9a7c4c318 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 8 Jan 2018 19:54:04 +0000 Subject: [PATCH 11/11] Adds changelog for backport of authorized_keys DB lookup from EE --- changelogs/unreleased/jej-backport-authorized-keys-to-ce.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/jej-backport-authorized-keys-to-ce.yml diff --git a/changelogs/unreleased/jej-backport-authorized-keys-to-ce.yml b/changelogs/unreleased/jej-backport-authorized-keys-to-ce.yml new file mode 100644 index 00000000000..4386c631f59 --- /dev/null +++ b/changelogs/unreleased/jej-backport-authorized-keys-to-ce.yml @@ -0,0 +1,5 @@ +--- +title: Backport fast database lookup of SSH authorized_keys from EE +merge_request: 16014 +author: +type: added