Commit 388de33b authored by Imre Farkas's avatar Imre Farkas

Require OTP for git+SSH

OTP verification can be done via an internal API call. Once it's
verified, a session is registered in Redis with the associated SSH key.
parent ad2a750e
......@@ -109,3 +109,43 @@ questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
## Two-factor Authentication (2FA) for Git over SSH operations
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/270554) in GitLab 13.7.
> - It's [deployed behind a feature flag](<replace with path to>/user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-two-factor-authentication-2fa-for-git-operations).
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
Two-factor authentication can be enforced for Git over SSH operations. The OTP
verification can be done via a GitLab Shell command:
```shell
ssh git@<hostname> 2fa_verify
```
Once the OTP is verified, Git over SSH operations can be used for 15 minutes
with the associated SSH key.
### Enable or disable Two-factor Authentication (2FA) for Git operations
Two-factor Authentication (2FA) for Git operations is under development and not
ready for production use. It is deployed behind a feature flag that is
**disabled by default**. [GitLab administrators with access to the GitLab Rails console](<replace with path to>/administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:two_factor_for_cli)
```
To disable it:
```ruby
Feature.disable(:two_factor_for_cli)
```
......@@ -300,7 +300,7 @@ module API
post '/two_factor_otp_check', feature_category: :authentication_and_authorization do
status 200
break { success: false } unless Feature.enabled?(:two_factor_for_cli)
break { success: false, message: 'Feature flag is disabled' } unless Feature.enabled?(:two_factor_for_cli)
actor.update_last_used_at!
user = actor.user
......@@ -316,6 +316,8 @@ module API
otp_validation_result = ::Users::ValidateOtpService.new(user).execute(params.fetch(:otp_attempt))
if otp_validation_result[:status] == :success
::Gitlab::Auth::Otp::SessionEnforcer.new(actor.key).update_session
{ success: true }
else
{ success: false, message: 'Invalid OTP' }
......
# frozen_string_literal: true
module Gitlab
module Auth
module Otp
class SessionEnforcer
OTP_SESSIONS_NAMESPACE = 'session:otp'
DEFAULT_EXPIRATION = 15.minutes.to_i
def initialize(key)
@key = key
end
def update_session
Gitlab::Redis::SharedState.with do |redis|
redis.setex(key_name, DEFAULT_EXPIRATION, true)
end
end
def access_restricted?
Gitlab::Redis::SharedState.with do |redis|
!redis.get(key_name)
end
end
private
attr_reader :key
def key_name
@key_name ||= "#{OTP_SESSIONS_NAMESPACE}:#{key.id}"
end
end
end
end
end
......@@ -77,6 +77,7 @@ module Gitlab
check_authentication_abilities!
check_command_disabled!
check_command_existence!
check_otp_session!
custom_action = check_custom_action
return custom_action if custom_action
......@@ -254,6 +255,31 @@ module Gitlab
end
end
def check_otp_session!
return unless ssh?
return if !key? || deploy_key?
return unless Feature.enabled?(:two_factor_for_cli)
return unless user.two_factor_enabled?
if ::Gitlab::Auth::Otp::SessionEnforcer.new(actor).access_restricted?
message = "OTP verification is required to access the repository.\n\n"\
" Use: #{build_ssh_otp_verify_command}"
raise ForbiddenError, message
end
end
def build_ssh_otp_verify_command
user = "#{Gitlab.config.gitlab_shell.ssh_user}@" unless Gitlab.config.gitlab_shell.ssh_user.empty?
user_host = "#{user}#{Gitlab.config.gitlab_shell.ssh_host}"
if Gitlab.config.gitlab_shell.ssh_port != 22
"ssh #{user_host} -p #{Gitlab.config.gitlab_shell.ssh_port} 2fa_verify"
else
"ssh #{user_host} 2fa_verify"
end
end
def check_db_accessibility!
return unless receive_pack?
......@@ -399,6 +425,10 @@ module Gitlab
protocol == 'http'
end
def ssh?
protocol == 'ssh'
end
def upload_pack?
cmd == 'git-upload-pack'
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Auth::Otp::SessionEnforcer, :clean_gitlab_redis_shared_state do
let_it_be(:key) { create(:key)}
describe '#update_session' do
it 'registers a session in Redis' do
redis = double(:redis)
expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
expect(redis).to(
receive(:setex)
.with("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}",
described_class::DEFAULT_EXPIRATION,
true)
.once)
described_class.new(key).update_session
end
end
describe '#access_restricted?' do
subject { described_class.new(key).access_restricted? }
context 'with existing session' do
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}", true )
end
end
it { is_expected.to be_falsey }
end
context 'without an existing session' do
it { is_expected.to be_truthy }
end
end
end
......@@ -387,6 +387,108 @@ RSpec.describe Gitlab::GitAccess do
end
end
describe '#check_otp_session!' do
let_it_be(:user) { create(:user, :two_factor_via_otp)}
let_it_be(:key) { create(:key, user: user) }
let_it_be(:actor) { key }
before do
project.add_developer(user)
stub_feature_flags(two_factor_for_cli: true)
end
context 'with an OTP session', :clean_gitlab_redis_shared_state do
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("#{Gitlab::Auth::Otp::SessionEnforcer::OTP_SESSIONS_NAMESPACE}:#{key.id}", true)
end
end
it 'allows push and pull access' do
aggregate_failures do
expect { push_access_check }.not_to raise_error
expect { pull_access_check }.not_to raise_error
end
end
end
context 'without OTP session' do
it 'does not allow push or pull access' do
user = 'jane.doe'
host = 'fridge.ssh'
port = 42
stub_config(
gitlab_shell: {
ssh_user: user,
ssh_host: host,
ssh_port: port
}
)
error_message = "OTP verification is required to access the repository.\n\n"\
" Use: ssh #{user}@#{host} -p #{port} 2fa_verify"
aggregate_failures do
expect { push_access_check }.to raise_forbidden(error_message)
expect { pull_access_check }.to raise_forbidden(error_message)
end
end
context 'when protocol is HTTP' do
let(:protocol) { 'http' }
it 'allows push and pull access' do
aggregate_failures do
expect { push_access_check }.not_to raise_error
expect { pull_access_check }.not_to raise_error
end
end
end
context 'when actor is not an SSH key' do
let(:deploy_key) { create(:deploy_key, user: user) }
let(:actor) { deploy_key }
before do
deploy_key.deploy_keys_projects.create(project: project, can_push: true)
end
it 'allows push and pull access' do
aggregate_failures do
expect { push_access_check }.not_to raise_error
expect { pull_access_check }.not_to raise_error
end
end
end
context 'when 2FA is not enabled for the user' do
let(:user) { create(:user)}
let(:actor) { create(:key, user: user) }
it 'allows push and pull access' do
aggregate_failures do
expect { push_access_check }.not_to raise_error
expect { pull_access_check }.not_to raise_error
end
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(two_factor_for_cli: false)
end
it 'allows push and pull access' do
aggregate_failures do
expect { push_access_check }.not_to raise_error
expect { pull_access_check }.not_to raise_error
end
end
end
end
end
describe '#check_db_accessibility!' do
context 'when in a read-only GitLab instance' do
before do
......
......@@ -1336,9 +1336,13 @@ RSpec.describe API::Internal::Base do
end
context 'when the OTP is valid' do
it 'returns success' do
it 'registers a new OTP session and returns success' do
allow_any_instance_of(Users::ValidateOtpService).to receive(:execute).with(otp).and_return(status: :success)
expect_next_instance_of(::Gitlab::Auth::Otp::SessionEnforcer) do |session_enforcer|
expect(session_enforcer).to receive(:update_session).once
end
subject
expect(json_response['success']).to be_truthy
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment