Skip to content
Snippets Groups Projects
Commit 11edbccc authored by Dylan Griffith's avatar Dylan Griffith
Browse files

Get mutual SSL working with helm tiller

parent ce897f11
No related branches found
No related tags found
1 merge request!10495Merge Requests - Assignee
Showing
with 328 additions and 59 deletions
require 'openssl'
module Clusters
module Applications
class Helm < ActiveRecord::Base
self.table_name = 'clusters_applications_helm'
 
attr_encrypted :ca_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
 
default_value_for :version, Gitlab::Kubernetes::Helm::HELM_VERSION
 
before_create :create_keys_and_certs
def create_keys_and_certs
ca_cert = Gitlab::Kubernetes::Helm::Certificate.generate_root
self.ca_key = ca_cert.key_string
self.ca_cert = ca_cert.cert_string
end
def ca_cert_obj
return unless has_ssl?
Gitlab::Kubernetes::Helm::Certificate
.from_strings(ca_key, ca_cert)
end
def issue_cert
ca_cert_obj
.issue
end
def set_initial_status
return unless not_installable?
 
Loading
Loading
@@ -15,11 +42,20 @@ module Clusters
end
 
def install_command
tiller_cert = issue_cert
Gitlab::Kubernetes::Helm::InitCommand.new(
name: name,
files: {}
files: {
'ca.pem': ca_cert,
'cert.pem': tiller_cert.cert_string,
'key.pem': tiller_cert.key_string
}
)
end
def has_ssl?
ca_key.present? && ca_cert.present?
end
end
end
end
Loading
Loading
@@ -13,9 +13,20 @@ module Clusters
end
 
def files
{
'values.yaml': values
}
@files ||= begin
files = { 'values.yaml': values }
if cluster.application_helm.has_ssl?
ca_cert = cluster.application_helm.ca_cert
helm_cert = cluster.application_helm.issue_cert
files.merge!({
'ca.pem': ca_cert,
'cert.pem': helm_cert.cert_string,
'key.pem': helm_cert.key_string
})
end
files
end
end
 
private
Loading
Loading
class AddColumnsForHelmTillerCertificates < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :clusters_applications_helm, :encrypted_ca_key, :text
add_column :clusters_applications_helm, :encrypted_ca_key_iv, :text
add_column :clusters_applications_helm, :ca_cert, :text
end
end
Loading
Loading
@@ -635,6 +635,9 @@ ActiveRecord::Schema.define(version: 20180722103201) do
t.integer "status", null: false
t.string "version", null: false
t.text "status_reason"
t.text "encrypted_ca_key"
t.text "encrypted_ca_key_iv"
t.text "ca_cert"
end
 
create_table "clusters_applications_ingress", force: :cascade do |t|
Loading
Loading
Loading
Loading
@@ -36,6 +36,10 @@ module Gitlab
 
private
 
def files_dir
"/data/helm/#{name}/config"
end
def namespace
Gitlab::Kubernetes::Helm::NAMESPACE
end
Loading
Loading
module Gitlab
module Kubernetes
module Helm
class Certificate
attr_reader :key, :cert
def key_string
@key.to_s
end
def cert_string
@cert.to_pem
end
def self.from_strings(key_string, cert_string)
key = OpenSSL::PKey::RSA.new(key_string)
cert = OpenSSL::X509::Certificate.new(cert_string)
new(key, cert)
end
def self.generate_root
key = OpenSSL::PKey::RSA.new(4096)
public_key = key.public_key
subject = "/C=US"
cert = OpenSSL::X509::Certificate.new
cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject)
cert.not_before = Time.now
cert.not_after = Time.now + 365 * 24 * 60 * 60
cert.public_key = public_key
cert.serial = 0x0
cert.version = 2
extension_factory = OpenSSL::X509::ExtensionFactory.new
extension_factory.subject_certificate = cert
extension_factory.issuer_certificate = cert
cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:TRUE', true))
cert.add_extension(extension_factory.create_extension('keyUsage', 'cRLSign,keyCertSign', true))
cert.sign key, OpenSSL::Digest::SHA256.new
new(key, cert)
end
def issue
key = OpenSSL::PKey::RSA.new(4096)
public_key = key.public_key
subject = "/C=US"
cert = OpenSSL::X509::Certificate.new
cert.subject = OpenSSL::X509::Name.parse(subject)
cert.issuer = self.cert.subject
cert.not_before = Time.now
cert.not_after = Time.now + 365 * 24 * 60 * 60
cert.public_key = public_key
cert.serial = 0x0
cert.version = 2
cert.sign self.key, OpenSSL::Digest::SHA256.new
self.class.new(key, cert)
end
private
def initialize(key, cert)
@key = key
@cert = cert
end
end
end
end
end
Loading
Loading
@@ -20,7 +20,12 @@ module Gitlab
private
 
def init_helm_command
"helm init >/dev/null"
tls_opts = "--tiller-tls" \
" --tiller-tls-verify --tls-ca-cert #{files_dir}/ca.pem" \
" --tiller-tls-cert #{files_dir}/cert.pem" \
" --tiller-tls-key #{files_dir}/key.pem"
"helm init #{tls_opts} >/dev/null"
end
end
end
Loading
Loading
Loading
Loading
@@ -34,8 +34,15 @@ module Gitlab
end
 
def script_command
if files.key?(:'ca.pem')
tls_opts = " --tls" \
" --tls-ca-cert #{files_dir}/ca.pem" \
" --tls-cert #{files_dir}/cert.pem" \
" --tls-key #{files_dir}/key.pem"
end
<<~HEREDOC
helm install #{chart} --name #{name}#{optional_version_flag} --namespace #{Gitlab::Kubernetes::Helm::NAMESPACE} -f /data/helm/#{name}/config/values.yaml >/dev/null
helm install#{tls_opts} #{chart} --name #{name}#{optional_version_flag} --namespace #{Gitlab::Kubernetes::Helm::NAMESPACE} -f /data/helm/#{name}/config/values.yaml >/dev/null
HEREDOC
end
 
Loading
Loading
Loading
Loading
@@ -32,11 +32,18 @@ FactoryBot.define do
updated_at ClusterWaitForAppInstallationWorker::TIMEOUT.ago
end
 
factory :clusters_applications_ingress, class: Clusters::Applications::Ingress
factory :clusters_applications_prometheus, class: Clusters::Applications::Prometheus
factory :clusters_applications_runner, class: Clusters::Applications::Runner
factory :clusters_applications_ingress, class: Clusters::Applications::Ingress do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
factory :clusters_applications_prometheus, class: Clusters::Applications::Prometheus do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
factory :clusters_applications_runner, class: Clusters::Applications::Runner do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
factory :clusters_applications_jupyter, class: Clusters::Applications::Jupyter do
oauth_application factory: :oauth_application
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
end
end
Loading
Loading
@@ -36,5 +36,9 @@ FactoryBot.define do
trait :production_environment do
sequence(:environment_scope) { |n| "production#{n}/*" }
end
trait :with_installed_helm do
application_helm factory: %i(clusters_applications_helm installed)
end
end
end
Loading
Loading
@@ -2,7 +2,7 @@ require 'spec_helper'
 
describe Gitlab::Kubernetes::Helm::InitCommand do
let(:application) { create(:clusters_applications_helm) }
let(:commands) { 'helm init >/dev/null' }
let(:commands) { 'helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem >/dev/null' }
 
subject { described_class.new(name: application.name, files: {}) }
 
Loading
Loading
require 'rails_helper'
 
describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:application) { create(:clusters_applications_prometheus) }
let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE }
let(:install_command) { application.install_command }
let(:files) { { 'ca.pem': 'some file content' } }
let(:repository) { 'https://repository.example.com' }
let(:version) { '1.2.3' }
 
subject { install_command }
let(:install_command) do
described_class.new(
name: 'app-name',
chart: 'chart-name',
files: files,
version: version, repository: repository
)
end
 
context 'for ingress' do
let(:application) { create(:clusters_applications_ingress) }
subject { install_command }
 
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
helm init --client-only >/dev/null
helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
EOS
end
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
helm init --client-only >/dev/null
helm repo add app-name https://repository.example.com
helm install --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem chart-name --name app-name --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null
EOS
end
end
 
context 'for prometheus' do
let(:application) { create(:clusters_applications_prometheus) }
context 'when there is no repository' do
let(:repository) { nil }
 
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
helm init --client-only >/dev/null
helm install #{application.chart} --name #{application.name} --version #{application.version} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
helm install --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem chart-name --name app-name --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null
EOS
end
end
end
 
context 'for runner' do
let(:ci_runner) { create(:ci_runner) }
let(:application) { create(:clusters_applications_runner, runner: ci_runner) }
context 'when there is no ca.pem file' do
let(:files) { { 'file.txt': 'some content' } }
 
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
helm init --client-only >/dev/null
helm repo add #{application.name} #{application.repository}
helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
helm repo add app-name https://repository.example.com
helm install chart-name --name app-name --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null
EOS
end
end
end
 
context 'for jupyter' do
let(:application) { create(:clusters_applications_jupyter) }
context 'when there is no version' do
let(:version) { nil }
 
it_behaves_like 'helm commands' do
let(:commands) do
<<~EOS
helm init --client-only >/dev/null
helm repo add #{application.name} #{application.repository}
helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
helm repo add app-name https://repository.example.com
helm install --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem chart-name --name app-name --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null
EOS
end
end
Loading
Loading
@@ -65,13 +70,13 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
describe '#config_map_resource' do
let(:metadata) do
{
name: "values-content-configuration-#{application.name}",
namespace: namespace,
labels: { name: "values-content-configuration-#{application.name}" }
name: "values-content-configuration-app-name",
namespace: 'gitlab-managed-apps',
labels: { name: "values-content-configuration-app-name" }
}
end
 
let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: application.files) }
let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) }
 
subject { install_command.config_map_resource }
 
Loading
Loading
Loading
Loading
@@ -2,8 +2,7 @@ require 'rails_helper'
 
describe Gitlab::Kubernetes::Helm::Pod do
describe '#generate' do
let(:cluster) { create(:cluster) }
let(:app) { create(:clusters_applications_prometheus, cluster: cluster) }
let(:app) { create(:clusters_applications_prometheus) }
let(:command) { app.install_command }
let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE }
 
Loading
Loading
Loading
Loading
@@ -6,13 +6,24 @@ describe Clusters::Applications::Helm do
describe '.installed' do
subject { described_class.installed }
 
let!(:cluster) { create(:clusters_applications_helm, :installed) }
let!(:installed_cluster) { create(:clusters_applications_helm, :installed) }
 
before do
create(:clusters_applications_helm, :errored)
end
 
it { is_expected.to contain_exactly(cluster) }
it { is_expected.to contain_exactly(installed_cluster) }
end
describe '#issue_cert' do
let(:application) { create(:clusters_applications_helm) }
subject { application.issue_cert }
it 'returns a new cert' do
is_expected.to be_kind_of(Gitlab::Kubernetes::Helm::Certificate)
expect(subject.cert_string).not_to eq(application.ca_cert)
expect(subject.key_string).not_to eq(application.ca_key)
end
end
 
describe '#install_command' do
Loading
Loading
@@ -25,5 +36,13 @@ describe Clusters::Applications::Helm do
it 'should be initialized with 1 arguments' do
expect(subject.name).to eq('helm')
end
it 'should have cert files' do
expect(subject.files[:'ca.pem']).to be_present
expect(subject.files[:'ca.pem']).to eq(helm.ca_cert)
expect(subject.files[:'cert.pem']).to be_present
expect(subject.files[:'key.pem']).to be_present
end
end
end
Loading
Loading
@@ -79,7 +79,9 @@ describe Clusters::Applications::Ingress do
end
 
describe '#files' do
let(:values) { ingress.files[:'values.yaml'] }
let(:application) { ingress }
subject { application.files }
let(:values) { subject[:'values.yaml'] }
 
it 'should include ingress valid keys in values' do
expect(values).to include('image')
Loading
Loading
@@ -87,5 +89,25 @@ describe Clusters::Applications::Ingress do
expect(values).to include('stats')
expect(values).to include('podAnnotations')
end
context 'when the helm application does not have a ca_cert' do
before do
application.cluster.application_helm.ca_cert = nil
end
it 'should not include cert files' do
expect(subject[:'ca.pem']).not_to be_present
expect(subject[:'cert.pem']).not_to be_present
expect(subject[:'key.pem']).not_to be_present
end
end
it 'should include cert files' do
expect(subject[:'ca.pem']).to be_present
expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
expect(subject[:'cert.pem']).to be_present
expect(subject[:'key.pem']).to be_present
end
end
end
Loading
Loading
@@ -43,9 +43,29 @@ describe Clusters::Applications::Jupyter do
end
 
describe '#files' do
let(:jupyter) { create(:clusters_applications_jupyter) }
let(:application) { create(:clusters_applications_jupyter) }
subject { application.files }
let(:values) { subject[:'values.yaml'] }
 
let(:values) { jupyter.files[:'values.yaml'] }
it 'should include cert files' do
expect(subject[:'ca.pem']).to be_present
expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
expect(subject[:'cert.pem']).to be_present
expect(subject[:'key.pem']).to be_present
end
context 'when the helm application does not have a ca_cert' do
before do
application.cluster.application_helm.ca_cert = nil
end
it 'should not include cert files' do
expect(subject[:'ca.pem']).not_to be_present
expect(subject[:'cert.pem']).not_to be_present
expect(subject[:'key.pem']).not_to be_present
end
end
 
it 'should include valid values' do
expect(values).to include('ingress')
Loading
Loading
@@ -53,8 +73,8 @@ describe Clusters::Applications::Jupyter do
expect(values).to include('rbac')
expect(values).to include('proxy')
expect(values).to include('auth')
expect(values).to match(/clientId: '?#{jupyter.oauth_application.uid}/)
expect(values).to match(/callbackUrl: '?#{jupyter.callback_url}/)
expect(values).to match(/clientId: '?#{application.oauth_application.uid}/)
expect(values).to match(/callbackUrl: '?#{application.callback_url}/)
end
end
end
Loading
Loading
@@ -158,9 +158,29 @@ describe Clusters::Applications::Prometheus do
end
 
describe '#files' do
let(:prometheus) { create(:clusters_applications_prometheus) }
let(:application) { create(:clusters_applications_prometheus) }
subject { application.files }
let(:values) { subject[:'values.yaml'] }
it 'should include cert files' do
expect(subject[:'ca.pem']).to be_present
expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
 
let(:values) { prometheus.files[:'values.yaml'] }
expect(subject[:'cert.pem']).to be_present
expect(subject[:'key.pem']).to be_present
end
context 'when the helm application does not have a ca_cert' do
before do
application.cluster.application_helm.ca_cert = nil
end
it 'should not include cert files' do
expect(subject[:'ca.pem']).not_to be_present
expect(subject[:'cert.pem']).not_to be_present
expect(subject[:'key.pem']).not_to be_present
end
end
 
it 'should include prometheus valid values' do
expect(values).to include('alertmanager')
Loading
Loading
Loading
Loading
@@ -38,11 +38,31 @@ describe Clusters::Applications::Runner do
end
 
describe '#files' do
let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
let(:application) { create(:clusters_applications_runner, runner: ci_runner) }
 
subject { gitlab_runner.files }
subject { application.files }
let(:values) { subject[:'values.yaml'] }
 
it 'should include cert files' do
expect(subject[:'ca.pem']).to be_present
expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
expect(subject[:'cert.pem']).to be_present
expect(subject[:'key.pem']).to be_present
end
context 'when the helm application does not have a ca_cert' do
before do
application.cluster.application_helm.ca_cert = nil
end
it 'should not include cert files' do
expect(subject[:'ca.pem']).not_to be_present
expect(subject[:'cert.pem']).not_to be_present
expect(subject[:'key.pem']).not_to be_present
end
end
it 'should include runner valid values' do
expect(values).to include('concurrent')
expect(values).to include('checkInterval')
Loading
Loading
@@ -57,8 +77,8 @@ describe Clusters::Applications::Runner do
 
context 'without a runner' do
let(:project) { create(:project) }
let(:cluster) { create(:cluster, projects: [project]) }
let(:gitlab_runner) { create(:clusters_applications_runner, cluster: cluster) }
let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
let(:application) { create(:clusters_applications_runner, cluster: cluster) }
 
it 'creates a runner' do
expect do
Loading
Loading
@@ -67,13 +87,13 @@ describe Clusters::Applications::Runner do
end
 
it 'uses the new runner token' do
expect(values).to match(/runnerToken: '?#{gitlab_runner.reload.runner.token}/)
expect(values).to match(/runnerToken: '?#{application.reload.runner.token}/)
end
 
it 'assigns the new runner to runner' do
subject
 
expect(gitlab_runner.reload.runner).to be_project_type
expect(application.reload.runner).to be_project_type
end
end
 
Loading
Loading
@@ -97,11 +117,11 @@ describe Clusters::Applications::Runner do
end
 
before do
allow(gitlab_runner).to receive(:chart_values).and_return(stub_values)
allow(application).to receive(:chart_values).and_return(stub_values)
end
 
it 'should overwrite values.yaml' do
expect(values).to match(/privileged: '?#{gitlab_runner.privileged}/)
expect(values).to match(/privileged: '?#{application.privileged}/)
end
end
end
Loading
Loading
Loading
Loading
@@ -47,7 +47,7 @@ describe Clusters::Applications::InstallService do
end
 
context 'when application cannot be persisted' do
let(:application) { build(:clusters_applications_helm, :scheduled) }
let(:application) { create(:clusters_applications_helm, :scheduled) }
 
it 'make the application errored' do
expect(application).to receive(:make_installing!).once.and_raise(ActiveRecord::RecordInvalid)
Loading
Loading
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