{"id":20631387,"url":"https://github.com/maxmelcher/gheaadproxy","last_synced_at":"2026-04-24T07:32:03.341Z","repository":{"id":68448105,"uuid":"368210422","full_name":"MaxMelcher/gheaadproxy","owner":"MaxMelcher","description":null,"archived":false,"fork":false,"pushed_at":"2021-11-11T14:19:03.000Z","size":793,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-01-17T07:09:54.900Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MaxMelcher.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-05-17T14:13:13.000Z","updated_at":"2021-11-11T14:19:06.000Z","dependencies_parsed_at":"2023-05-20T03:56:45.267Z","dependency_job_id":null,"html_url":"https://github.com/MaxMelcher/gheaadproxy","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxMelcher%2Fgheaadproxy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxMelcher%2Fgheaadproxy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxMelcher%2Fgheaadproxy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MaxMelcher%2Fgheaadproxy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MaxMelcher","download_url":"https://codeload.github.com/MaxMelcher/gheaadproxy/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242619114,"owners_count":20159001,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-16T14:12:08.696Z","updated_at":"2026-04-24T07:31:58.249Z","avatar_url":"https://github.com/MaxMelcher.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# GitHub Enterprise Server with Azure Active Directory Authentication (GHEAADPROXY)\n\nAlso, see this blog post: https://melcher.dev/2021/05/github-enterprise-server-with-azure-ad-auth-git-mfa/\n\nWith GitHub Enterprise Server Version 3.0.6 as self-hosted VM, you can configure SAML authentication backed by Azure Active Directory. \nThis protects the web interface, you get conditional access policies with Multi Factor Authentiction powered by Azure Active Directory. \nSessions do expire after a configurable lifetime, then the user must re-authenticate.\n\nUnfortunately, this does not apply for the git side of things. If you clone a repository, pull or push a username and PAT token or ssh key is being accepted. \nThere is no session or multi-factor enforcement. \n\nWith this **proof of concept (PoC)** I want to showcase that it is possible to put a proxy in front of GitHub Enterprise Server that enforces MFA (based on the configuration in Azure Active Directory) and that the session expires.\n\n## High Level Architecture\n\nFrom left to right we have three components: [Azure Application Gateway](https://docs.microsoft.com/en-us/azure/application-gateway/overview),  [Azure Kubernetes Service](https://docs.microsoft.com/en-us/azure/aks/)(AKS), and lastly GitHub Enterprise Server (GHE) installed in a single VM on Azure. The AKS hosts the authentication proxy container, based on a .NET 5 in Alpine linux (107MB).\n\nThe following architecture depicts the two authentication flows.\n![](images/highlevel.png)\n\nWhen a user wants to browse to the web-frontend of GitHub Enterprise (GHE) server, the traffic will be filtered on Azure Application Gateway/ Web Application Firewall (WAF). Afterwards, the traffic will hit the proxy. If there is no AUTHORIZATION bearer header, then the proxy will send a HTTP 302 redirect to Azure Active Directory. The user must sign in and depending on the configuration provide the second factor. Then the request is sent back to the WAF, reaches the proxy and then will be sent unmodified to GHE. \n\n![](images/ghe-web.png)\n\nWhen a user wants to use the git client, things become more challenging. The client itself is pretty simple on an auth perspective, but provides several options to piggyback additional authorization headers to GHE. The challenge is that GHE does not accept multiple AUTHORIZATION headers, if you provide them it will simply reject the request with a HTTP 400. Additionally the client does not authenticate un-asked. The first request is always unauthenticated, then gets a HTTP 401 by the server, then authenticates with username/password or username/personal access token (PAT).\n\nIf the proxy receives an openid connect bearer token, it will validate it. Afterwards it will send the request to GHE without the additional header - multiple or unexpected headers will lead to HTTP 400. \n\nThe following picture shows the git pull experience without a valid bearer token:\n![](images/git-redirect.png)\nThe git client does not follow HTTP 302 redirects, no additional authentication will happen.\n\nBut it is really simple to get a valid token - the following powershell gets one and puts it in the git config file. This config can be local or global.\nIt will be submitted to the proxy, stripped from the request and the actual request will go to GHE:\n![](images/git-bearer.png)\n\nScript to get the token:  \n\u003ccode\u003e\n$bearer = $(az account get-access-token --resource api://253600a6-46b5-425f-ab7e-d70df34c97ae --query accessToken)  \ngit config --global http.extraHeader \"MFA: bearer $($bearer)\"\n\u003c/code\u003e\n\n## Deployment\n\nTo deploy this, you need two Azure Active Directory Applications. One Enterprise Application that handles the SAML authentication. The second one is for the openid connect authentication. I tried to combine it, but then only SAML works. \n\nI started with the SAML authentication for the GHE server - for this you need to register a new Enterprise Application. Only there you can configure the SAML URLs and get the certificate for GHE.\n![](images/ghe-saml.png)\n\nThe AAD documentation describes how to [setup SAML with Azure Active Directory](https://docs.microsoft.com/en-us/azure/active-directory/saas-apps/github-tutorial).\n\nThe URL https://ghe.francecentral.cloudapp.azure.com points to the frontend IP of the WAF.\n\nThe second AAD application is 'only' an Application registration:\n![](images/ghe-openidc.png) \nIt has the reply URL set to /signin-openidc and configured Access and ID tokens.\n\nThe proxy is using this application, therefore you need the application ID. \nIt must be stored in the appsettings.json configuration or as a docker environment variable (AzureAd__ClientId).\nAdditionally you need to expose an API, this is then needed to generate the access token via azure cli as shown above. The --resource flag points to that API url. The client id 04b07795-8ddb-461a-bbee-02f9e1bf7b46 is the one of Azure CLI: \n![](images/app-api.png)\n\n## Kubernetes Deployment\n\nI used the [Azure Application Gateway Kubernetes Ingress](https://github.com/Azure/application-gateway-kubernetes-ingress) to manage the WAF directly from the AKS. Additionally I wanted to have [valid SSL certificates provided by LetsEncrypt](https://github.com/Azure/application-gateway-kubernetes-ingress/blob/master/docs/how-tos/lets-encrypt.md).\nOnce the core infrastructure was running, I deployed the container, a service and ingress as shown in the [deploy.yaml](deploy.yaml).\n\nSome settings are hardcoded in the [appsettings.json](appsettings.json) - they can be overwritten during the kubernetes deployment. \n\n## OpenSource Components: \n* YARP: Yet Another Reverse Proxy (https://github.com/microsoft/reverse-proxy)\n* [Microsoft.Identity.Web](https://github.com/AzureAD/microsoft-identity-web) to handle openid connect, JWT tokens.\n\n## Questions / Feedback\n\nYou read so far? Wow - would be happy to get in touch, this is an exotic PoC, let me know if you have questions.\n\n## License and Disclaimer\n\nIt is a proof of concept - see [license](license.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxmelcher%2Fgheaadproxy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaxmelcher%2Fgheaadproxy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxmelcher%2Fgheaadproxy/lists"}