Skip to content
Snippets Groups Projects
Unverified Commit b6316689 authored by Imre (Admin)'s avatar Imre (Admin)
Browse files

Smartcard authentication with basic X.509 cert

This adds support for smartcard authentication with basic X.509
certificates. NGINX needs to be setup in a way to run the same server
context on separate port requesting the client side certificate. Another
limitation is that the email address must be a part of the Common-Name
attribute.
parent b875431f
No related branches found
No related tags found
No related merge requests found
Showing
with 289 additions and 8 deletions
Loading
Loading
@@ -189,11 +189,11 @@ def can?(object, action, subject = :global)
Ability.allowed?(object, action, subject)
end
 
def access_denied!(message = nil)
def access_denied!(message = nil, status = nil)
# If we display a custom access denied message to the user, we don't want to
# hide existence of the resource, rather tell them they cannot access it using
# the provided message
status = message.present? ? :forbidden : :not_found
status ||= message.present? ? :forbidden : :not_found
 
respond_to do |format|
format.any { head status }
Loading
Loading
Loading
Loading
@@ -24,6 +24,23 @@ def label_for_provider(name)
Gitlab::Auth::OAuth::Provider.label_for(name)
end
 
def form_based_provider_priority
['crowd', /^ldap/, 'kerberos']
end
def form_based_provider_with_highest_priority
@form_based_provider_with_highest_priority ||= begin
form_based_provider_priority.each do |provider_regexp|
highest_priority = form_based_providers.find { |provider| provider.match?(provider_regexp) }
break highest_priority unless highest_priority.nil?
end
end
end
def form_based_auth_provider_has_active_class?(provider)
form_based_provider_with_highest_priority == provider
end
def form_based_provider?(name)
[LDAP_PROVIDER, 'crowd'].any? { |pattern| pattern === name.to_s }
end
Loading
Loading
- if form_based_providers.any?
- if crowd_enabled?
.login-box.tab-pane.active{ id: "crowd", role: 'tabpanel' }
.login-box.tab-pane{ id: "crowd", role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:crowd)) }
.login-body
= render 'devise/sessions/new_crowd'
 
- if kerberos_enabled?
.login-box.tab-pane{ id: "kerberos", role: 'tabpanel', class: (:active unless crowd_enabled? || ldap_enabled?) }
.login-box.tab-pane{ id: "kerberos", role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:kerberos)) }
.login-body
= render 'devise/sessions/new_kerberos'
 
- @ldap_servers.each_with_index do |server, i|
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && !crowd_enabled?) }
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain)) }
.login-body
= render 'devise/sessions/new_ldap', server: server
= render_if_exists 'devise/sessions/new_smartcard'
- if password_authentication_enabled_for_web?
.login-box.tab-pane{ id: 'login-pane', role: 'tabpanel' }
.login-body
Loading
Loading
%ul.nav-links.new-session-tabs.nav-tabs.nav{ class: ('custom-provider-tabs' if form_based_providers.any?) }
- if crowd_enabled?
%li.nav-item
= link_to "Crowd", "#crowd", class: 'nav-link active', 'data-toggle' => 'tab'
= link_to "Crowd", "#crowd", class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:crowd))}", 'data-toggle' => 'tab'
- if kerberos_enabled?
%li.nav-item
= link_to "Kerberos", "#kerberos", class: "nav-link #{active_when(!crowd_enabled? && !ldap_enabled?)}", 'data-toggle' => 'tab'
= link_to "Kerberos", "#kerberos", class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:kerberos))}", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
%li.nav-item
= link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && !crowd_enabled?)} qa-ldap-tab", 'data-toggle' => 'tab'
= link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain))} qa-ldap-tab", 'data-toggle' => 'tab'
= render_if_exists 'devise/shared/tab_smartcard'
- if password_authentication_enabled_for_web?
%li.nav-item
= link_to 'Standard', '#login-pane', class: 'nav-link qa-standard-tab', 'data-toggle' => 'tab'
Loading
Loading
Loading
Loading
@@ -550,6 +550,17 @@ production: &base
# host:
# ....
 
## Smartcard authentication settings
smartcard:
# Allow smartcard authentication
enabled: false
# Path to a file containing a CA certificate
ca_file: '/etc/ssl/certs/CA.pem'
# Port where the client side certificate is requested by the webserver (NGINX/Apache)
# client_certificate_required_port: 3444
## Kerberos settings
kerberos:
# Allow the HTTP Negotiate authentication method for Git clients
Loading
Loading
Loading
Loading
@@ -50,6 +50,9 @@
end
end
 
Settings['smartcard'] ||= Settingslogic.new({})
Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil?
Settings['omniauth'] ||= Settingslogic.new({})
Settings.omniauth['enabled'] = true if Settings.omniauth['enabled'].nil?
Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil?
Loading
Loading
Loading
Loading
@@ -89,6 +89,7 @@
 
draw :operations
draw :instance_statistics
draw :smartcard
 
if ENV['GITLAB_ENABLE_CHAOS_ENDPOINTS']
get '/chaos/leakmem' => 'chaos#leakmem'
Loading
Loading
Loading
Loading
@@ -2581,6 +2581,14 @@
t.index ["team_id", "alias"], name: "index_slack_integrations_on_team_id_and_alias", unique: true, using: :btree
end
 
create_table "smartcard_identities", id: :bigserial, force: :cascade do |t|
t.integer "user_id", null: false
t.string "subject", null: false
t.string "issuer", null: false
t.index ["subject", "issuer"], name: "index_smartcard_identities_on_subject_and_issuer", unique: true, using: :btree
t.index ["user_id"], name: "index_smartcard_identities_on_user_id", using: :btree
end
create_table "snippets", force: :cascade do |t|
t.string "title"
t.text "content"
Loading
Loading
@@ -3293,6 +3301,7 @@
add_foreign_key "saml_providers", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "slack_integrations", "services", on_delete: :cascade
add_foreign_key "smartcard_identities", "users", on_delete: :cascade
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
add_foreign_key "software_license_policies", "projects", on_delete: :cascade
add_foreign_key "software_license_policies", "software_licenses", on_delete: :cascade
Loading
Loading
Loading
Loading
@@ -16,3 +16,4 @@ providers.
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [Okta](okta.md) Configure GitLab to sign in using Okta
- [Authentiq](authentiq.md): Enable the Authentiq OmniAuth provider for passwordless authentication
- [Smartcard](smartcard.md) Smartcard authentication
# Smartcard authentication
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/726) in
[GitLab Premium](https://about.gitlab.com/pricing/) 11.5 as an experimental
feature. Smartcard authentication may change or be removed completely in future
releases.
Smartcards with X.509 certificates can be used to authenticate with GitLab.
## X.509 certificates
To use a smartcard with an X.509 certificate to authenticate with GitLab, `CN`
and `emailAddress` must be defined in the certificate. For example:
```
Certificate:
Data:
Version: 1 (0x0)
Serial Number: 12856475246677808609 (0xb26b601ecdd555e1)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=Random Corp Ltd, CN=Random Corp
Validity
Not Before: Oct 30 12:00:00 2018 GMT
Not After : Oct 30 12:00:00 2019 GMT
Subject: CN=Gitlab User, emailAddress=gitlab-user@example.com
```
## Configure NGINX to request a client side certificate
In NGINX configuration, an **additional** server context must be defined with
the same configuration except:
- The additional NGINX server context must be configured to run on a different
port:
```
listen *:3444 ssl;
```
- The additional NGINX server context must be configured to require the client
side certificate:
```
ssl_verify_depth 2;
ssl_client_certificate /etc/ssl/certs/CA.pem;
ssl_verify_client on;
```
- The additional NGINX server context must be configured to forward the client
side certificate:
```
proxy_set_header X-SSL-Client-Certificate $ssl_client_escaped_cert;
```
For example, the following is an example server context in an NGINX
configuration file (eg. in `/etc/nginx/sites-available/gitlab-ssl`):
```
server {
listen *:3444 ssl;
# certificate for configuring SSL
ssl_certificate /path/to/example.com.crt;
ssl_certificate_key /path/to/example.com.key;
ssl_verify_depth 2;
# CA certificate for client side certificate verification
ssl_client_certificate /etc/ssl/certs/CA.pem;
ssl_verify_client on;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-SSL-Client-Certificate $ssl_client_escaped_cert;
proxy_read_timeout 300;
proxy_pass http://gitlab-workhorse;
}
}
```
## Configure GitLab for smartcard authentication
**For installations from source**
1. Edit `config/gitlab.yml`:
```yaml
## Smartcard authentication settings
smartcard:
# Allow smartcard authentication
enabled: true
# Path to a file containing a CA certificate
ca_file: '/etc/ssl/certs/CA.pem'
# Port where the client side certificate is requested by NGINX
client_certificate_required_port: 3444
```
1. Save the file and restart GitLab for the changes to take effect.
# frozen_string_literal: true
class SmartcardController < ApplicationController
skip_before_action :authenticate_user!
skip_before_action :verify_authenticity_token
before_action :check_feature_availability
before_action :check_certificate_headers
def auth
certificate = Gitlab::Auth::Smartcard::Certificate.new(CGI.unescape(certificate_header))
user = certificate.find_or_create_user
unless user
flash[:alert] = _('Failed to signing using smartcard authentication')
redirect_to new_user_session_path(port: Gitlab.config.gitlab.port)
return
end
log_audit_event(user, with: 'smartcard')
sign_in_and_redirect(user)
end
protected
def check_feature_availability
render_404 unless ::Gitlab::Auth::Smartcard.enabled?
end
def check_certificate_headers
# Failing on requests coming from the port not requiring client side certificate
unless certificate_header.present?
access_denied!(_('Smartcard authentication failed: client certificate header is missing.'), 401)
end
end
def log_audit_event(user, options = {})
AuditEventService.new(user, user, options).for_authentication.security_event
end
def certificate_header
request.headers['HTTP_X_SSL_CLIENT_CERTIFICATE']
end
def after_sign_in_path_for(resource)
stored_location_for(:redirect) || stored_location_for(resource) || root_url(port: Gitlab.config.gitlab.port)
end
end
Loading
Loading
@@ -16,15 +16,33 @@ def providers_for_base_controller
super - GROUP_LEVEL_PROVIDERS
end
 
override :form_based_provider_priority
def form_based_provider_priority
super << 'smartcard'
end
override :form_based_provider?
def form_based_provider?(name)
super || name.to_s == 'kerberos'
end
 
override :form_based_providers
def form_based_providers
providers = super
providers << :smartcard if smartcard_enabled?
providers
end
def kerberos_enabled?
auth_providers.include?(:kerberos)
end
 
def smartcard_enabled?
::Gitlab::Auth::Smartcard.enabled?
end
def slack_redirect_uri(project)
slack_auth_project_settings_slack_url(project)
end
Loading
Loading
Loading
Loading
@@ -45,6 +45,8 @@ module User
has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ::ProtectedBranch::PushAccessLevel # rubocop:disable Cop/ActiveRecordDependent
has_many :protected_branch_unprotect_access_levels, dependent: :destroy, class_name: ::ProtectedBranch::UnprotectAccessLevel # rubocop:disable Cop/ActiveRecordDependent
 
has_many :smartcard_identities
scope :excluding_guests, -> { joins(:members).where('members.access_level > ?', ::Gitlab::Access::GUEST).distinct }
 
scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) }
Loading
Loading
@@ -77,6 +79,11 @@ def non_ldap
joins('LEFT JOIN identities ON identities.user_id = users.id')
.where('identities.provider IS NULL OR identities.provider NOT LIKE ?', 'ldap%')
end
def find_by_smartcard_identity(certificate_subject, certificate_issuer)
joins(:smartcard_identities)
.find_by(smartcard_identities: { subject: certificate_subject, issuer: certificate_issuer })
end
end
 
def cannot_be_admin_and_auditor
Loading
Loading
Loading
Loading
@@ -57,6 +57,7 @@ class License < ActiveRecord::Base
object_storage
group_saml
service_desk
smartcard_auth
unprotection_restrictions
variable_environment_scope
reject_unsigned_commits
Loading
Loading
# frozen_string_literal: true
class SmartcardIdentity < ActiveRecord::Base
belongs_to :user
validates :user,
presence: true
validates :subject,
presence: true,
uniqueness: { scope: :issuer }
validates :issuer,
presence: true
end
module EE
module Users
module BuildService
extend ::Gitlab::Utils::Override
override :execute
def execute(skip_authorization: false)
user = super
build_smartcard_identity(user, params) if ::Gitlab::Auth::Smartcard.enabled?
user
end
private
 
def signup_params
Loading
Loading
@@ -15,6 +26,15 @@ def email_opted_in_params
:email_opted_in_at
]
end
def build_smartcard_identity(user, params)
smartcard_identity_attrs = params.slice(:certificate_subject, :certificate_issuer)
if smartcard_identity_attrs.any?
user.smartcard_identities.build(subject: params[:certificate_subject],
issuer: params[:certificate_issuer])
end
end
end
end
end
- if smartcard_enabled?
.login-box.tab-pane{ id: 'smartcard', role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:smartcard)) }
.login-body
= form_tag(smartcard_auth_url(port: Gitlab.config.smartcard.client_certificate_required_port), html: { 'aria-live' => 'assertive'}) do
.submit-container
= submit_tag _('Login with smartcard'), class: 'btn btn-success'
- if smartcard_enabled?
%li.nav-item
= link_to _('Smartcard'), '#smartcard', class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:smartcard))}", 'data-toggle' => 'tab'
---
title: Smartcard authentication
merge_request: 8120
author:
type: added
# frozen_string_literal: true
post 'smartcard/auth' => 'smartcard#auth'
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment