Commit 420d6771 authored by Thong Kuah's avatar Thong Kuah

Merge branch '8605-support-san-extension' into 'master'

Allow SmartCard authentication to use SAN extensions

Closes #8605

See merge request gitlab-org/gitlab-ee!15773
parents b497b40a 133a3e11
...@@ -718,6 +718,10 @@ production: &base ...@@ -718,6 +718,10 @@ production: &base
# Browser session with smartcard sign-in is required for Git access # Browser session with smartcard sign-in is required for Git access
# required_for_git_access: false # required_for_git_access: false
# Use X.509 SAN extensions certificates to identify GitLab users
# Add a subjectAltName to your certificates like: email:user
# san_extensions: true
## Kerberos settings ## Kerberos settings
kerberos: kerberos:
# Allow the HTTP Negotiate authentication method for Git clients # Allow the HTTP Negotiate authentication method for Git clients
......
...@@ -78,6 +78,7 @@ Gitlab.ee do ...@@ -78,6 +78,7 @@ Gitlab.ee do
Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil? Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil?
Settings.smartcard['client_certificate_required_port'] = 3444 if Settings.smartcard['client_certificate_required_port'].nil? Settings.smartcard['client_certificate_required_port'] = 3444 if Settings.smartcard['client_certificate_required_port'].nil?
Settings.smartcard['required_for_git_access'] = false if Settings.smartcard['required_for_git_access'].nil? Settings.smartcard['required_for_git_access'] = false if Settings.smartcard['required_for_git_access'].nil?
Settings.smartcard['san_extensions'] = false if Settings.smartcard['san_extensions'].nil?
end end
Settings['omniauth'] ||= Settingslogic.new({}) Settings['omniauth'] ||= Settingslogic.new({})
......
...@@ -39,6 +39,45 @@ Certificate: ...@@ -39,6 +39,45 @@ Certificate:
Subject: CN=Gitlab User, emailAddress=gitlab-user@example.com Subject: CN=Gitlab User, emailAddress=gitlab-user@example.com
``` ```
### Authentication against a local database with X.509 certificates and SAN extensions **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/8605) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
Smartcards with X.509 certificates using SAN extensions can be used to authenticate
with GitLab.
NOTE: **Note:**
This is an experimental feature. Smartcard authentication against local databases may
change or be removed completely in future releases.
To use a smartcard with an X.509 certificate to authenticate against a local
database with GitLab, at least one of the `subjectAltName` (SAN) extensions
need to define the user identity (`email`) within the GitLab instance (`URI`).
`URI`: needs to match `Gitlab.config.host.gitlab`.
For example:
```text
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
...
X509v3 extensions:
X509v3 Key Usage:
Key Encipherment, Data Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Subject Alternative Name:
email:gitlab-user@example.com, URI:http://gitlab.example.com/
```
### Authentication against an LDAP server ### Authentication against an LDAP server
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7693) in > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7693) in
...@@ -152,6 +191,36 @@ attribute. As a prerequisite, you must use an LDAP server that: ...@@ -152,6 +191,36 @@ attribute. As a prerequisite, you must use an LDAP server that:
1. Save the file and [restart](../restart_gitlab.md#installations-from-source) 1. Save the file and [restart](../restart_gitlab.md#installations-from-source)
GitLab for the changes to take effect. GitLab for the changes to take effect.
### Additional steps when using SAN extensions
**For Omnibus installations**
1. Add to `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['smartcard_san_extensions'] = true
```
1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure)
GitLab for the changes to take effect.
**For installations from source**
1. Add the `san_extensions` line to config/gitlab.yml` within the smartcard section:
```yaml
smartcard:
enabled: true
ca_file: '/etc/ssl/certs/CA.pem'
client_certificate_required_port: 3444
# Enable the use of SAN extensions to match users with certificates
san_extensions: true
```
1. Save the file and [restart](../restart_gitlab.md#installations-from-source)
GitLab for the changes to take effect.
### Additional steps when authenticating against an LDAP server ### Additional steps when authenticating against an LDAP server
**For Omnibus installations** **For Omnibus installations**
......
---
title: Allow SmartCard authentication to use SAN extensions
merge_request: 15773
author:
type: added
...@@ -4,6 +4,8 @@ module Gitlab ...@@ -4,6 +4,8 @@ module Gitlab
module Auth module Auth
module Smartcard module Smartcard
class Certificate < Gitlab::Auth::Smartcard::Base class Certificate < Gitlab::Auth::Smartcard::Base
include Gitlab::Utils::StrongMemoize
def auth_method def auth_method
'smartcard' 'smartcard'
end end
...@@ -53,7 +55,21 @@ module Gitlab ...@@ -53,7 +55,21 @@ module Gitlab
end end
def email def email
subject.split('/').find { |part| part =~ /emailAddress=/ }&.remove('emailAddress=')&.strip strong_memoize(:email) do
if san_enabled?
san_extension.email_identity
else
subject.split('/').find { |part| part =~ /emailAddress=/ }&.remove('emailAddress=')&.strip
end
end
end
def san_enabled?
Gitlab.config.smartcard.san_extensions
end
def san_extension
@san_extension ||= SANExtension.new(@certificate, Gitlab.config.gitlab.host)
end end
def username def username
......
# frozen_string_literal: true
module Gitlab
module Auth
module Smartcard
class SANExtension
# From X.509 RFC https://tools.ietf.org/html/rfc5280
# A.2. Implicitly Tagged Module, 1988 Syntax ....................
# page-127
# Names TAG Type
##########################################################
# otherName [0] OtherName
# rfc822Name(email) [1] IA5String
# dNSName [2] IA5String
# x400Address [3] ORAddress
# directoryName [4] Name
# ediPartyName [5] EDIPartyName
# uniformResourceIdentifier [6] IA5String
# iPAddress [7] OCTET STRING
# registeredID [8] OBJECT IDENTIFIER
EMAIL_TAG = 1
URI_TAG = 6
def initialize(certificate, gitlab_host)
@certificate = certificate
@gitlab_host = gitlab_host
end
def email_identity
alternate_emails.find { |name| gitlab_host?(name[URI_TAG]) }&.fetch(EMAIL_TAG, nil)
end
def alternate_emails
@alternate_emails ||= subject_alternate_email_identities
end
private
attr_reader :certificate, :gitlab_host
def subject_alternate_email_identities
subject_alt_names = certificate.extensions.select {|e| e.oid == 'subjectAltName'}
subject_alt_names.each_with_object([]) do |entry, san_entries|
# Parse the subject alternate name certificate extension as ASN1, first value should be the key
asn_san = OpenSSL::ASN1.decode(entry)
# And the second value should be a nested ASN1 sequence
asn_san_sequence = OpenSSL::ASN1.decode(asn_san.value[1].value)
san_entries << asn_san_sequence.each_with_object({}) do |asn_data, alternate_names|
alternate_names[asn_data.tag] = asn_data.value if [EMAIL_TAG, URI_TAG].include?(asn_data.tag)
end
end
end
def gitlab_host?(uri)
URI.parse(uri).host == gitlab_host
rescue URI::InvalidURIError
false
end
end
end
end
end
...@@ -117,6 +117,24 @@ describe Gitlab::Auth::Smartcard::Certificate do ...@@ -117,6 +117,24 @@ describe Gitlab::Auth::Smartcard::Certificate do
end end
end end
end end
context 'san email defined' do
let(:san_defined_email) { 'san@domain.email' }
before do
allow(Gitlab.config.smartcard).to receive(:san_extensions).and_return(true)
expect_next_instance_of(Gitlab::Auth::Smartcard::SANExtension) do |san_extension|
expect(san_extension).to receive(:email_identity).and_return(san_defined_email)
end
end
it 'creates user' do
expect { subject }.to change { User.count }.from(0).to(1)
expect(User.first.email).to eql(san_defined_email)
end
end
end end
it_behaves_like 'a valid certificate is required' it_behaves_like 'a valid certificate is required'
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Auth::Smartcard::SANExtension do
let(:fqdn) { 'gitlab.example.com' }
let(:extension_factory) { OpenSSL::X509::ExtensionFactory.new(nil, cert) }
let(:san_extension) { described_class.new(cert, fqdn)}
let(:cert) do
key = OpenSSL::PKey::RSA.new 2048
name = OpenSSL::X509::Name.parse 'CN=nobody/DC=example'
cert = OpenSSL::X509::Certificate.new
cert.version = 3
cert.serial = 0
cert.not_before = Time.now
cert.not_after = Time.now + 3600
cert.public_key = key.public_key
cert.subject = name
cert
end
def add_san_entry(value)
cert.add_extension extension_factory.create_extension('subjectAltName', value)
end
describe '#alternate_emails' do
subject { san_extension.alternate_emails }
context 'without SAN extensions' do
it { is_expected.to be_empty }
end
context 'with SAN extensions' do
describe 'single extension' do
let(:uri) { 'https://gitlab.example.com' }
before do
add_san_entry "URI:#{uri}"
end
it { is_expected.to match([{ described_class::URI_TAG => uri }]) }
end
describe 'multiple entries using ASN1' do
let(:email) { 'my@other.address' }
let(:uri) { '1.2.3.4' }
before do
add_san_entry "email:#{email},URI:#{uri}"
end
it {
is_expected.to match([{
described_class::EMAIL_TAG => email,
described_class::URI_TAG => uri
}])
}
end
describe 'custom General Name' do
it 'can\'t use custom alt names that are not part of general names' do
expect { add_san_entry 'customName:some@gitlab.com' }
.to raise_error OpenSSL::X509::ExtensionError
end
end
end
end
describe '#email_identity' do
let(:san_email) { 'newemail@some.domain' }
let(:san_uri) { "https://#{fqdn}" }
before do
allow(Gitlab.config.gitlab).to receive(:host).and_return(fqdn)
end
subject { san_extension.email_identity }
describe 'alternate name email for GitLab defined in the certificate' do
before do
add_san_entry "email:#{san_email},URI:#{san_uri}"
end
it { is_expected.to eq san_email }
describe 'inappropriate URI format' do
let(:san_uri) { 'an invalid uri' }
it { is_expected.to be_nil }
end
end
describe 'no alternate name defined to use with GitLab' do
it { is_expected.to be_nil }
end
context 'when the host is partially matched to the URI' do
let(:forged_uri) { "https://#{fqdn}.anotherdomain.com" }
let(:forged_identity) { 'hacker@email.com' }
before do
add_san_entry "email:#{forged_identity},URI:#{forged_uri}"
end
it { is_expected.to be_nil }
end
end
end
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