GitHub Enterprise Server (GHES) & Entra IDでSSOを設定する手順。
備忘録として纏めます。
前提
- GitHub Enterprise Server (GHES) ライセンスを持っていること
- Entra ID 管理者権限を持っていること
GHES用 仮想マシンの作成
AzureでGHES用の仮想マシンを構築します。
以下の最小要件を参照しながら進めます。
今回は検証用途で、作ったり壊してを繰り返したかったのでIaCに。
以下、Terraform 👇️
サンプルコード
terraform {
required_version = ">=0.12"
required_providers {
azapi = {
source = "azure/azapi"
}
azurerm = {
source = "hashicorp/azurerm"
}
random = {
source = "hashicorp/random"
}
}
}
provider "azurerm" {
features {}
}
variable "location" {
type = string
default = "japaneast"
}
variable "prefix" {
type = string
default = "ghes"
}
variable "resource_group_name" {
type = string
default = "rg-ghes"
}
variable "host_name" {
type = string
description = "value of vm host name"
default = "×××××××××××"
}
variable "username" {
type = string
default = "azureuser"
}
resource "random_pet" "ssh_key_name" {
prefix = "ssh"
separator = ""
}
resource "azapi_resource_action" "ssh_public_key_gen" {
type = "Microsoft.Compute/sshPublicKeys@2022-11-01"
resource_id = azapi_resource.ssh_public_key.id
action = "generateKeyPair"
method = "POST"
response_export_values = ["publicKey", "privateKey"]
}
resource "azapi_resource" "ssh_public_key" {
type = "Microsoft.Compute/sshPublicKeys@2022-11-01"
name = random_pet.ssh_key_name.id
location = azurerm_resource_group.rg.location
parent_id = azurerm_resource_group.rg.id
}
resource "local_file" "public_key_file" {
content = azapi_resource_action.ssh_public_key_gen.output.publicKey
filename = "./id_rsa.pub"
}
resource "local_file" "private_key_file" {
content = azapi_resource_action.ssh_public_key_gen.output.privateKey
filename = "./id_rsa"
}
resource "azurerm_resource_group" "rg" {
location = var.location
name = var.resource_group_name
}
# Vnet
resource "azurerm_virtual_network" "vnet" {
address_space = ["10.0.0.0/16"]
location = var.location
name = "vnet-${var.prefix}"
resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_subnet" "subnet_default" {
address_prefixes = ["10.0.0.0/24"]
name = "default"
resource_group_name = var.resource_group_name
virtual_network_name = azurerm_virtual_network.vnet.name
}
resource "azurerm_public_ip" "public_ip" {
allocation_method = "Static"
domain_name_label = var.host_name
location = var.location
name = "ip-${var.prefix}"
resource_group_name = var.resource_group_name
sku = "Standard"
depends_on = [
azurerm_resource_group.rg,
]
}
resource "azurerm_network_interface" "nic" {
location = var.location
name = "nic-${var.prefix}"
resource_group_name = var.resource_group_name
ip_configuration {
name = "ipconfig1"
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.public_ip.id
subnet_id = azurerm_subnet.subnet_default.id
}
}
resource "azurerm_network_security_group" "nsg" {
location = var.location
name = "nsg-${var.prefix}"
resource_group_name = var.resource_group_name
depends_on = [
azurerm_resource_group.rg,
]
}
resource "azurerm_network_interface_security_group_association" "nsgassoc" {
network_interface_id = azurerm_network_interface.nic.id
network_security_group_id = azurerm_network_security_group.nsg.id
}
resource "azurerm_network_security_rule" "allow_http_inbound" {
access = "Allow"
destination_address_prefix = "*"
destination_port_range = "80"
direction = "Inbound"
name = "AllowAnyHTTPInbound"
network_security_group_name = azurerm_network_security_group.nsg.name
priority = 1025
protocol = "Tcp"
resource_group_name = var.resource_group_name
source_address_prefix = "*"
source_port_range = "*"
}
resource "azurerm_network_security_rule" "allow_git_end_user" {
access = "Allow"
destination_address_prefix = "*"
destination_port_range = "9418"
direction = "Inbound"
name = "Git-End_user"
network_security_group_name = azurerm_network_security_group.nsg.name
priority = 1050
protocol = "Tcp"
resource_group_name = var.resource_group_name
source_address_prefix = "*"
source_port_range = "*"
}
resource "azurerm_network_security_rule" "allow_https_end_user" {
access = "Allow"
destination_address_prefix = "*"
destination_port_range = "443"
direction = "Inbound"
name = "HTTPS-End_user"
network_security_group_name = azurerm_network_security_group.nsg.name
priority = 1030
protocol = "Tcp"
resource_group_name = var.resource_group_name
source_address_prefix = "*"
source_port_range = "*"
}
resource "azurerm_network_security_rule" "allow_https_management_console" {
access = "Allow"
destination_address_prefix = "*"
destination_port_range = "8443"
direction = "Inbound"
name = "HTTPS-Management_console"
network_security_group_name = azurerm_network_security_group.nsg.name
priority = 1010
protocol = "Tcp"
resource_group_name = var.resource_group_name
source_address_prefix = "*"
source_port_range = "*"
}
resource "azurerm_network_security_rule" "allow_ssh_end_user" {
access = "Allow"
destination_address_prefix = "*"
destination_port_range = "22"
direction = "Inbound"
name = "SSH-End_user"
network_security_group_name = azurerm_network_security_group.nsg.name
priority = 1040
protocol = "Tcp"
resource_group_name = var.resource_group_name
source_address_prefix = "*"
source_port_range = "*"
}
resource "azurerm_network_security_rule" "allow_ssh_management_console" {
access = "Allow"
destination_address_prefix = "*"
destination_port_range = "122"
direction = "Inbound"
name = "SSH-Management_console"
network_security_group_name = azurerm_network_security_group.nsg.name
priority = 1020
protocol = "Tcp"
resource_group_name = var.resource_group_name
source_address_prefix = "*"
source_port_range = "*"
}
# add data disk
resource "azurerm_managed_disk" "data_disk" {
create_option = "Empty"
location = var.location
name = "data-disk-${var.prefix}-0"
resource_group_name = var.resource_group_name
storage_account_type = "Premium_LRS"
disk_size_gb = 512
depends_on = [
azurerm_resource_group.rg,
]
}
# vm
resource "azurerm_linux_virtual_machine" "vm" {
admin_username = var.username
location = var.location
name = "vm-${var.prefix}"
network_interface_ids = [azurerm_network_interface.nic.id]
resource_group_name = var.resource_group_name
size = "Standard_E4s_v3"
# Generate SSH key
admin_ssh_key {
public_key = azapi_resource_action.ssh_public_key_gen.output.publicKey
username = var.username
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}
source_image_reference {
offer = "GitHub-Enterprise"
publisher = "GitHub"
sku = "github-enterprise-gen2"
version = "latest"
}
}
resource "azurerm_virtual_machine_data_disk_attachment" "diskattach" {
caching = "ReadWrite"
lun = 0
managed_disk_id = azurerm_managed_disk.data_disk.id
virtual_machine_id = azurerm_linux_virtual_machine.vm.id
}
GHESの初期セットアップ
デプロイしたVMのホスト名にアクセス
(立ち上がりまでちょっと待つ)
ラインセンスファイルをアップロードして管理サイト用のパスワードを入力
「Set UP a new Instance」 を選択し、初期セットアップ画面に進み、まずはデフォルトのままセットアップを完了させる。
これまたしばらく待つ。
完了し、Enterpriseオーナーの作成画面が出てきたら、アカウントを作成する。
このアカウントは認証方法をSAMLに変更後も、予備的にID・パスワードでログインできるようにしておくためのもの。そのため、SAML認証に使用しないアドレス、ユーザー名にしておくこと。
ログインして、右上の 🚀 > Management Console > 管理サイト画面へ移動しておく。
Entra ID 側の設定
以下のドキュメントを参考に進める。
Entra ID > Enterpriseアプリケーション > 新しいアプリケーション > GitHub Enterprise Server
-
GitHub Enterprise Server アプリケーション設定に移動
-
左側のサイドバーの [シングル サインオン] をクリックし、[SAML] を選択
-
[基本的な SAML 構成] セクションで、[編集] をクリックし、以下を追加
- 識別子:
https://<HOSTNAME>.com
- 応答 URL:
https://<HOSTNAME>.com/saml/consume
- サインオン URL:
https://<HOSTNAME>.com/sso
- 識別子:
-
[属性とクレーム] セクションで [編集] をクリックし、次のクレームを追加
- full_name
- Join (user.givenname, " ", user.surname)
- emails
- user.userprincipalname
- administrator
- 要求条件:
- ユーザータイプ: メンバー
- グループ: グループを指定
- ソース: 属性
- 値:
true
- (例) Adminグループに属しているメンバーはEnterpriseオーナー & 管理サイト権限が付くようにする。SAML属性の詳細はこちらを参照
- 要求条件:
- full_name
5.[SAML 認定資格証] セクションで、SAML認定資格証 (Base64) をダウンロード
6. [GitHub Enterprise Serverの設定] セクションで、ログインURLとMicrosoft Entra識別子をコピーしておく
7. 左側サイドバーの [ユーザーとグループ] からユーザーを割り当てる
GHES 側の設定変更
ここからは下記ドキュメントを参考に。
管理サイト画面に戻り、Atutenticationセクションで [SAML] を選択。
-
[Allow built-in authentication] (組み込み認証を許可)
- チェックをいれる
- シングルサインオンだけでなく、ID, パスワードでも入れるようにしておく
- チェックをいれる
-
Single sign-on URL
- 先ほどコピーしたログインURL
-
Issuer
- Microsoft Entra 識別子
-
Verification certificate
- ダウンロードしたSAML認定資格証をアップロード
-
User attributes
full_name
emails
public_keys
gpg_keys
設定後は以下のようになる。
ここまで設定できたら、保存して更新をかけてしばらく待つ。
完了すると、ログイン画面が以下の様になり、SSOできるようになる。
カスタム属性のマッピングができていれば、管理者権限になってるはず。
「Sign in with username and password」のリンクが画面下部に表示されているため、うまくいかない場合は組み込みのオーナーアカウントのIDとパスワードでログインして設定を確認できる。
さいごに (というか備考)
IDPでユーザーを割り当て、または割り当て解除するときに、IDPはGHESと自動的に通信していない。ユーザーが初めてGHESにアクセスし、IDPを介して認証し、サインインしたときに、GHESは SAML Just-in-Time (JIT) プロビジョニングを使用してユーザー アカウントを作成している。
このJITプロビジョニングでは、IDPからユーザーを削除した場合、GHESインスタンス上のユーザーアカウントを手動で停止する必要がある。
これはめんどくさい。
そこで、自動的にプロビジョニングされるよう、SCIMを設定できる。
が…GHESでこの機能はまだベータ版のよう。
手順通りにやってみようとしたが、個人アクセストークン(Classic) のスコープにscim:enterprise
が見当たらなかった。(なんで?😱)
これに関しては、もう少し調べてから、また今度やってみる事にする。