Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--changelogs/unreleased/34572-ssh-certificates.yml5
-rw-r--r--doc/administration/operations/fast_ssh_key_lookup.md7
-rw-r--r--doc/administration/operations/index.md5
-rw-r--r--doc/administration/operations/ssh_certificates.md165
-rw-r--r--lib/api/internal.rb55
-rw-r--r--spec/requests/api/internal_spec.rb65
7 files changed, 280 insertions, 24 deletions
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 0ee843cc604..ae9a76b9249 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-7.2.0
+8.0.0
diff --git a/changelogs/unreleased/34572-ssh-certificates.yml b/changelogs/unreleased/34572-ssh-certificates.yml
new file mode 100644
index 00000000000..76a08a188de
--- /dev/null
+++ b/changelogs/unreleased/34572-ssh-certificates.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for SSH certificate authentication
+merge_request: 19911
+author: Ævar Arnfjörð Bjarmason
+type: added
diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md
index 89331238ce4..752a2774bd7 100644
--- a/doc/administration/operations/fast_ssh_key_lookup.md
+++ b/doc/administration/operations/fast_ssh_key_lookup.md
@@ -1,3 +1,10 @@
+# Consider using SSH certificates instead of, or in addition to this
+
+This document describes a drop-in replacement for the
+`authorized_keys` file for normal (non-deploy key) users. Consider
+using [ssh certificates](ssh_certificates.md), they are even faster,
+but are not is not a drop-in replacement.
+
# Fast lookup of authorized SSH keys in the database
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/1631) in
diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md
index 5655b7efec6..e9cad99c4b0 100644
--- a/doc/administration/operations/index.md
+++ b/doc/administration/operations/index.md
@@ -14,4 +14,7 @@ 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](fast_ssh_key_lookup.md): Authorize SSH users via a fast, indexed lookup to the GitLab database.
+- Speed up SSH operations by [Authorizing SSH users via a fast,
+indexed lookup to the GitLab database](fast_ssh_key_lookup.md), and/or
+by [doing away with user SSH keys stored on GitLab entirely in favor
+of SSH certificates](ssh_certificates.md).
diff --git a/doc/administration/operations/ssh_certificates.md b/doc/administration/operations/ssh_certificates.md
new file mode 100644
index 00000000000..8968afba01b
--- /dev/null
+++ b/doc/administration/operations/ssh_certificates.md
@@ -0,0 +1,165 @@
+# User lookup via OpenSSH's AuthorizedPrincipalsCommand
+
+> [Available in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19911) GitLab
+> Community Edition 11.2.
+
+GitLab's default SSH authentication requires users to upload their ssh
+public keys before they can use the SSH transport.
+
+In centralized (e.g. corporate) environments this can be a hassle
+operationally, particularly if the SSH keys are temporary keys issued
+to the user, e.g. ones that expire 24 hours after issuing.
+
+In such setups some external automated process is needed to constantly
+upload the new keys to GitLab.
+
+> **Warning:** OpenSSH version 6.9+ is required because that version
+introduced the `AuthorizedPrincipalsCommand` configuration option. If
+using CentOS 6, you can [follow these
+instructions](fast_ssh_key_lookup.html#compiling-a-custom-version-of-openssh-for-centos-6)
+to compile an up-to-date version.
+
+## Why use OpenSSH certificates?
+
+By using OpenSSH certificates all the information about what user on
+GitLab owns the key is encoded in the key itself, and OpenSSH itself
+guarantees that users can't fake this, since they'd need to have
+access to the private CA signing key.
+
+When correctly set up, this does away with the requirement of
+uploading user SSH keys to GitLab entirely.
+
+## Setting up SSH certificate lookup via GitLab Shell
+
+How to fully setup SSH certificates is outside the scope of this
+document. See [OpenSSH's
+PROTOCOL.certkeys](https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD)
+for how it works, and e.g. [RedHat's documentation about
+it](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/deployment_guide/sec-using_openssh_certificate_authentication).
+
+We assume that you already have SSH certificates set up, and have
+added the `TrustedUserCAKeys` of your CA to your `sshd_config`, e.g.:
+
+```
+TrustedUserCAKeys /etc/security/mycompany_user_ca.pub
+```
+
+Usually `TrustedUserCAKeys` would not be scoped under a `Match User
+git` in such a setup, since it would also be used for system logins to
+the GitLab server itself, but your setup may vary. If the CA is only
+used for GitLab consider putting this in the `Match User git` section
+(described below).
+
+The SSH certificates being issued by that CA **MUST** have a "key id"
+corresponding to that user's username on GitLab, e.g. (some output
+omitted for brevity):
+
+```
+$ ssh-add -L | grep cert | ssh-keygen -L -f -
+(stdin):1:
+ Type: ssh-rsa-cert-v01@openssh.com user certificate
+ Public key: RSA-CERT SHA256:[...]
+ Signing CA: RSA SHA256:[...]
+ Key ID: "aearnfjord"
+ Serial: 8289829611021396489
+ Valid: from 2018-07-18T09:49:00 to 2018-07-19T09:50:34
+ Principals:
+ sshUsers
+ [...]
+ [...]
+```
+
+Technically that's not strictly true, e.g. it could be
+`prod-aearnfjord` if it's a SSH certificate you'd normally log in to
+servers as the `prod-aearnfjord` user, but then you must specify your
+own `AuthorizedPrincipalsCommand` to do that mapping instead of using
+our provided default.
+
+The important part is that the `AuthorizedPrincipalsCommand` must be
+able to map from the "key id" to a GitLab username in some way, the
+default command we ship assumes there's a 1=1 mapping between the two,
+since the whole point of this is to allow us to extract a GitLab
+username from the key itself, instead of relying on something like the
+default public key to username mapping.
+
+Then, in your `sshd_config` set up `AuthorizedPrincipalsCommand` for
+the `git` user. Hopefully you can use the default one shipped with
+GitLab:
+
+```
+Match User git
+ AuthorizedPrincipalsCommandUser root
+ AuthorizedPrincipalsCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-principals-check %i sshUsers
+```
+
+This command will emit output that looks something like:
+
+```
+command="/opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell username-{KEY_ID}",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty {PRINCIPAL}
+```
+
+Where `{KEY_ID}` is the `%i` argument passed to the script
+(e.g. `aeanfjord`), and `{PRINCIPAL}` is the principal passed to it
+(e.g. `sshUsers`).
+
+You will need to customize the `sshUsers` part of that. It should be
+some principal that's guaranteed to be part of the key for all users
+who can log in to GitLab, or you must provide a list of principals,
+one of which is going to be present for the user, e.g.:
+
+```
+ [...]
+ AuthorizedPrincipalsCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-principals-check %i sshUsers windowsUsers
+```
+
+## Principals and security
+
+You can supply as many principals as you want, these will be turned
+into multiple lines of `authorized_keys` output, as described in the
+`AuthorizedPrincipalsFile` documentation in `sshd_config(5)`.
+
+Normally when using the `AuthorizedKeysCommand` with OpenSSH the
+principal is some "group" that's allowed to log into that
+server. However with GitLab it's only used to appease OpenSSH's
+requirement for it, we effectively only care about the "key id" being
+correct. Once that's extracted GitLab will enforce its own ACLs for
+that user (e.g. what projects the user can access).
+
+So it's OK to e.g. be overly generous in what you accept, since if the
+user e.g. has no access to GitLab at all it'll just error out with a
+message about this being an invalid user.
+
+## Interaction with the `authorized_keys` file
+
+SSH certificates can be used in conjunction with the `authorized_keys`
+file, and if setup as configured above the `authorized_keys` file will
+still serve as a fallback.
+
+This is because if the `AuthorizedPrincipalsCommand` can't
+authenticate the user, OpenSSH will fall back on
+`~/.ssh/authorized_keys` (or the `AuthorizedKeysCommand`).
+
+Therefore there may still be a reason to use the ["Fast lookup of
+authorized SSH keys in the database"](fast_ssh_key_lookup.html) method
+in conjunction with this. Since you'll be using SSH certificates for
+all your normal users, and relying on the `~/.ssh/authorized_keys`
+fallback for deploy keys, if you make use of those.
+
+But you may find that there's no reason to do that, since all your
+normal users will use the fast `AuthorizedPrincipalsCommand` path, and
+only automated deployment key access will fall back on
+`~/.ssh/authorized_keys`, or that you have a lot more keys for normal
+users (especially if they're renewed) than you have deploy keys.
+
+## Other security caveats
+
+Users can still bypass SSH certificate authentication by manually
+uploading an SSH public key to their profile, relying on the
+`~/.ssh/authorized_keys` fallback to authenticate it. There's
+currently no feature to prevent this, [but there's an open request for
+adding it](https://gitlab.com/gitlab-org/gitlab-ce/issues/49218).
+
+Such a restriction can currently be hacked in by e.g. providing a
+custom `AuthorizedKeysCommand` which checks if the discovered key-ID
+returned from `gitlab-shell-authorized-keys-check` is a deploy key or
+not (all non-deploy keys should be refused).
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index a9803be9f69..516f25db15b 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -11,7 +11,8 @@ module API
#
# Params:
# key_id - ssh key id for Git over SSH
- # user_id - user id for Git over HTTP
+ # user_id - user id for Git over HTTP or over SSH in keyless SSH CERT mode
+ # username - user name for Git over SSH in keyless SSH cert mode
# protocol - Git access protocol being used, e.g. HTTP or SSH
# project - project full_path (not path on disk)
# action - git action (git-upload-pack or git-receive-pack)
@@ -28,6 +29,8 @@ module API
Key.find_by(id: params[:key_id])
elsif params[:user_id]
User.find_by(id: params[:user_id])
+ elsif params[:username]
+ User.find_by_username(params[:username])
end
protocol = params[:protocol]
@@ -58,6 +61,7 @@ module API
{
status: true,
gl_repository: gl_repository,
+ gl_id: Gitlab::GlId.gl_id(user),
gl_username: user&.username,
# This repository_path is a bogus value but gitlab-shell still requires
@@ -71,10 +75,17 @@ module API
post "/lfs_authenticate" do
status 200
- key = Key.find(params[:key_id])
- key.update_last_used_at
+ if params[:key_id]
+ actor = Key.find(params[:key_id])
+ actor.update_last_used_at
+ elsif params[:user_id]
+ actor = User.find_by(id: params[:user_id])
+ raise ActiveRecord::RecordNotFound.new("No such user id!") unless actor
+ else
+ raise ActiveRecord::RecordNotFound.new("No key_id or user_id passed!")
+ end
- token_handler = Gitlab::LfsToken.new(key)
+ token_handler = Gitlab::LfsToken.new(actor)
{
username: token_handler.actor_name,
@@ -100,7 +111,7 @@ module API
end
#
- # Discover user by ssh key or user id
+ # Discover user by ssh key, user id or username
#
get "/discover" do
if params[:key_id]
@@ -108,6 +119,8 @@ module API
user = key.user
elsif params[:user_id]
user = User.find_by(id: params[:user_id])
+ elsif params[:username]
+ user = User.find_by(username: params[:username])
end
present user, with: Entities::UserSafe
@@ -141,22 +154,30 @@ module API
post '/two_factor_recovery_codes' do
status 200
- key = Key.find_by(id: params[:key_id])
+ if params[:key_id]
+ key = Key.find_by(id: params[:key_id])
- if key
- key.update_last_used_at
- else
- break { 'success' => false, 'message' => 'Could not find the given key' }
- end
+ if key
+ key.update_last_used_at
+ else
+ break { 'success' => false, 'message' => 'Could not find the given key' }
+ end
- if key.is_a?(DeployKey)
- break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
- end
+ if key.is_a?(DeployKey)
+ break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
+ end
+
+ user = key.user
- user = key.user
+ unless user
+ break { success: false, message: 'Could not find a user for the given key' }
+ end
+ elsif params[:user_id]
+ user = User.find_by(id: params[:user_id])
- unless user
- break { success: false, message: 'Could not find a user for the given key' }
+ unless user
+ break { success: false, message: 'Could not find the given user' }
+ end
end
unless user.two_factor_enabled?
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index a2cfa706f58..b537b6e1667 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -152,7 +152,7 @@ describe API::Internal do
context 'user key' do
it 'returns the correct information about the key' do
- lfs_auth(key.id, project)
+ lfs_auth_key(key.id, project)
expect(response).to have_gitlab_http_status(200)
expect(json_response['username']).to eq(user.username)
@@ -161,8 +161,30 @@ describe API::Internal do
expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
end
+ it 'returns the correct information about the user' do
+ lfs_auth_user(user.id, project)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['username']).to eq(user.username)
+ expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(user).token)
+
+ expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
+ end
+
+ it 'returns a 404 when no key or user is provided' do
+ lfs_auth_project(project)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
it 'returns a 404 when the wrong key is provided' do
- lfs_auth(nil, project)
+ lfs_auth_key(key.id + 12345, project)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns a 404 when the wrong user is provided' do
+ lfs_auth_user(user.id + 12345, project)
expect(response).to have_gitlab_http_status(404)
end
@@ -172,7 +194,7 @@ describe API::Internal do
let(:key) { create(:deploy_key) }
it 'returns the correct information about the key' do
- lfs_auth(key.id, project)
+ lfs_auth_key(key.id, project)
expect(response).to have_gitlab_http_status(200)
expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}")
@@ -183,13 +205,29 @@ describe API::Internal do
end
describe "GET /internal/discover" do
- it do
+ it "finds a user by key id" do
get(api("/internal/discover"), key_id: key.id, secret_token: secret_token)
expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(user.name)
end
+
+ it "finds a user by user id" do
+ get(api("/internal/discover"), user_id: user.id, secret_token: secret_token)
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['name']).to eq(user.name)
+ end
+
+ it "finds a user by username" do
+ get(api("/internal/discover"), username: user.username, secret_token: secret_token)
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['name']).to eq(user.name)
+ end
end
describe "GET /internal/authorized_keys" do
@@ -871,7 +909,15 @@ describe API::Internal do
)
end
- def lfs_auth(key_id, project)
+ def lfs_auth_project(project)
+ post(
+ api("/internal/lfs_authenticate"),
+ secret_token: secret_token,
+ project: project.full_path
+ )
+ end
+
+ def lfs_auth_key(key_id, project)
post(
api("/internal/lfs_authenticate"),
key_id: key_id,
@@ -879,4 +925,13 @@ describe API::Internal do
project: project.full_path
)
end
+
+ def lfs_auth_user(user_id, project)
+ post(
+ api("/internal/lfs_authenticate"),
+ user_id: user_id,
+ secret_token: secret_token,
+ project: project.full_path
+ )
+ end
end