<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mark Schmale</title>
    <description>The latest articles on DEV Community by Mark Schmale (@themasch).</description>
    <link>https://dev.to/themasch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F92692%2F491a13be-c2b1-44b0-abc0-4b70209e3183.jpeg</url>
      <title>DEV Community: Mark Schmale</title>
      <link>https://dev.to/themasch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/themasch"/>
    <language>en</language>
    <item>
      <title>simple static gitlab review apps using nginx </title>
      <dc:creator>Mark Schmale</dc:creator>
      <pubDate>Thu, 30 Aug 2018 13:20:30 +0000</pubDate>
      <link>https://dev.to/themasch/simple-gitlab-static-review-apps-using-nginx--2d9</link>
      <guid>https://dev.to/themasch/simple-gitlab-static-review-apps-using-nginx--2d9</guid>
      <description>&lt;h2&gt;
  
  
  The Motivation
&lt;/h2&gt;

&lt;p&gt;When working on front-end projects for customers I always wanted to be able to show them the current state without having to deploy to the test environment all the time. I'd like to be able to have multiple versions, one per branch, so multiple people could work on different things but have deployed, world-reachable version to show and discuss with colleagues or customers. This would also improve merge requests since one could not only review the code but also review the deployed project in the browser without having to check out the code. &lt;/p&gt;

&lt;p&gt;This is exactly what GitLab calls &lt;a href="https://docs.gitlab.com/ee/ci/review_apps/"&gt;"Review Apps"&lt;/a&gt;. It is basically using their CI &lt;a href="https://docs.gitlab.com/ee/ci/environments.html"&gt;environments&lt;/a&gt; feature and showing them in merge requests. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Implementation
&lt;/h2&gt;

&lt;p&gt;The recommended way seems to be to do this using docker &amp;amp; kubernetes but getting the resources for even a small cluster is not cheap and setting it up just for better code reviews for a small portion of the code is not something I wanted to do. So I went with a much simpler approach. I got a small VM, set up nginx with a wildcard vhost &lt;code&gt;$name.review.example.com&lt;/code&gt; and configured this to serve files from &lt;code&gt;/var/www/$name/public&lt;/code&gt;. It cannot execute backend code or proxy to other backends, it only serves static files.&lt;/p&gt;

&lt;p&gt;The nginx config for this is pretty straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="s"&gt;[::]:443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;^(?&amp;lt;name&amp;gt;.+?).review.exampe.com$;&lt;/span&gt;

  &lt;span class="s"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="n"&gt;/public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;# insert your TLS config here. https://mozilla.github.io/server-side-tls/ssl-config-generator/ is a great tool!&lt;/span&gt;

  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/pages-access.log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;error_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/pages-error.log&lt;/span&gt; &lt;span class="s"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuration for gzip, general TLS stuff, sendfile etc. is done globally in the &lt;code&gt;nginx.conf&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The deployment works via a gitlab shell runner running on the same machine that just rsyncs the code to that target directory. The GitLab CI scripts looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;build_app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                                                                                                                                                                                                                                  
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;                                                                                                                                                                                                                              
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node&lt;/span&gt;                                                                                                                                                                                                                               
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                                                                                                                                                                                                                                   
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm install&lt;/span&gt;                                                                                                                                                                                                                             
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm run build-release&lt;/span&gt;                                                                                                                                                                                                                   
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                                                                                                                                                                                                                                     
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;branches&lt;/span&gt;                                                                                                                                                                                                                                
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                                                                                                                                                                                                                                
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                                                                                                                                                                                                                                  
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;public/&lt;/span&gt;                                                                                                                                                                                                                               

&lt;span class="na"&gt;deploy_review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                                                                                                                                                                                                                              
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;review&lt;/span&gt;                                                                                                                                                                                                                             
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                                                                                                                                                                                                                                   
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mkdir -p "/var/www/$REVIEW_TAG/public"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rsync -drv --delete public/ "/var/www/$REVIEW_TAG/public/"&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review/$CI_BUILD_REF_NAME"&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://$CI_PROJECT_ID-$CI_BUILD_REF_SLUG.review.example.com&lt;/span&gt;
    &lt;span class="na"&gt;on_stop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stop_review&lt;/span&gt;
  &lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build_app&lt;/span&gt;
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;branches&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;review&lt;/span&gt;

&lt;span class="na"&gt;stop_review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;review&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rm -rf "/var/www/$REVIEW_TAG"&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;GIT_STRATEGY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
  &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manual&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review/$CI_BUILD_REF_NAME"&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stop&lt;/span&gt;
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;branches&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;review&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stop_review task can be triggered manually or is used by gitlab when the MR is closed and the branch deleted. It just removes the docroot. The tag &lt;code&gt;review&lt;/code&gt; is used to select the correct runner for these jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authenti-what?
&lt;/h3&gt;

&lt;p&gt;This works great. It builds the code on our internal docker ci runner, puts the result in an archive, downloads the archive onto the review server and rsyncs the content to the document root. The deploy_review job takes about 3-4 seconds for most apps. From there on the public folder is served publicly via &lt;code&gt;https://&amp;lt;project-ID&amp;gt;-&amp;lt;branch name&amp;gt;.review.example.com&lt;/code&gt;. Without authentication. That's easy, and may not be a problem for some cases, but it definitely does not feel good when you deploy projects developed for customers there, maybe even containing real-world test data. &lt;/p&gt;

&lt;p&gt;So some sort of authentication was required. Since the ease of use was one of the core features on this, hiding it behind a complex SSO system was not an option (also there is no such system currently in use here). HTTP basic auth would be the perfect fit but maintaining &lt;code&gt;.htpasswd&lt;/code&gt; files is painful if the number of users gets large and it only allows for authentication. But when we have authentication why stop there? We could add some authorization to the mix so we can allow only a certain set of users to access any given project. &lt;/p&gt;

&lt;h3&gt;
  
  
  nginx subrequests to the rescue
&lt;/h3&gt;

&lt;p&gt;Thankfully there's an &lt;del&gt;app&lt;/del&gt; nginx module for that. The &lt;a href="http://nginx.org/en/docs/http/ngx_http_auth_request_module.html"&gt;ngx_http_auth_request module&lt;/a&gt; allows you to ask a third party to authenticate the user. On the server side, nginx creates a subrequest to that third party and checks the response status code. If it is 200 it continues to serve the clients request, if it is 401 or 403 it returns the same error to the client, passing through the WWW-Authenticate header for 401 results. Every other status code is considered to be an error (HTTP 500 for the client). &lt;/p&gt;

&lt;p&gt;The additions to the nginx config look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;auth_request&lt;/span&gt; &lt;span class="n"&gt;/_remote_auth_/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/_remote_auth/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;https://auth.example.com/remote_auth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="kn"&gt;proxy_pass_request_body&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Content-Length&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="kn"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We added the &lt;code&gt;auth_request&lt;/code&gt; directive to the main location context and added another, &lt;code&gt;internal&lt;/code&gt; location context to proxy the subrequest to our authentication backend. Using the proxy here allows us to have some more control over the requests, for example removing the request body (if there is any) and unsetting its content length. &lt;/p&gt;

&lt;h3&gt;
  
  
  Overengineering is my biggest strength
&lt;/h3&gt;

&lt;p&gt;So with the nginx side solved, we need a backend to answer our authentication requests. I decided to roll a small Symfony application that just answers HTTP requests on a single route and requires HTTP basic auth on it, using users from a database. This setup allows for future extensions, like checking the requested domain name against a list of allowed products or adding different storage backends like LDAP. &lt;/p&gt;

&lt;p&gt;Building something like this in PHP using Symfony as a framework can actually be done quite quickly, it involves about 200LoC in PHP, most of what can be generated (bootstrap code, db migration, orm entities) and a few dozen lines of configuration. For convenience, we are using the &lt;a href="https://symfony.com/doc/master/bundles/EasyAdminBundle/index.html"&gt;EasyAdminBundle&lt;/a&gt; to provide a simple user interface for administrators to create and modify users. &lt;/p&gt;

&lt;p&gt;I choose to run the authentication backend on the same server as the review app since its currently the only service using it. Deployment is easy, since we already have gitlab runner there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deploy_to_prod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;  
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rsync -drv --exclude '.env' --exclude '.*' --exclude 'vendor' --exclude 'var/' --delete . "/var/www/authcenter/"&lt;/span&gt;  
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cd /var/www/authcenter/&lt;/span&gt;  
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -O "https://getcomposer.org/download/1.7.2/composer.phar"&lt;/span&gt;  
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;php composer.phar install --no-dev&lt;/span&gt;  
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;php bin/console doctrine:migrations:migrate --no-interaction&lt;/span&gt;  
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;APP_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;
    &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$PROD_DATABASE_URL&lt;/span&gt;  
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;authcenter prod&lt;/span&gt;  
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://auth.example.com&lt;/span&gt;
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;  
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nginx config for this is not complex at all. I just used the &lt;a href="https://symfony.com/doc/current/setup/web_server_configuration.html#nginx"&gt;template provided by symfony&lt;/a&gt; and just tweaked it to fit my &lt;code&gt;PHP-fpm&lt;/code&gt; config. I also made use of the &lt;code&gt;fastcgi_param&lt;/code&gt; directive to set the &lt;code&gt;APP_ENV&lt;/code&gt; and &lt;code&gt;DATABASE_URL&lt;/code&gt; environment variables required by the Symfony application.&lt;/p&gt;

&lt;p&gt;I can sign in into the application using &lt;a href="https://auth.example.com/admin"&gt;https://auth.example.com/admin&lt;/a&gt; and get the EasyAdmin interface to modify or create users, or use &lt;a href="https://auth.example.com/remote_auth"&gt;https://auth.example.com/remote_auth&lt;/a&gt; and just get an empty HTTP 200 response. &lt;/p&gt;

&lt;p&gt;When browsing one of my review apps I'm asked to enter my basic auth credentials on the first request and every following request receives the same credentials because that's how browsers do this stuff. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Discovery Of Slowness
&lt;/h3&gt;

&lt;p&gt;This works, but I soon recognized that it's slow. Actually, terrible slow for something that's "just serving static files". Requests took up to 1second, sometimes even more, per file. That number should be well below 100ms! I went and tried to debug/profile the PHP application. My first suspicion was that opcache would be missing. And really, that module was not installed. But that did not fix my problem. It was faster, but not fast. I was prepared to get into the dirty work profiling this really small application on an OS level when I recognized that giant "WARNING: performance killer ahead" sign I should have seen when I started this project. &lt;br&gt;
The current implementation worked as follows. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user's browser sends a request to the server, containing the credentials in the Authorization header. &lt;/li&gt;
&lt;li&gt;nginx starts subrequest, handing the credentials over to my PHP application. &lt;/li&gt;
&lt;li&gt;PHP queries the database for the username&lt;/li&gt;
&lt;li&gt;PHP verifies the password using &lt;strong&gt;bcrypt&lt;/strong&gt;. &lt;/li&gt;
&lt;li&gt;If the password matches, it responds with HTTP 200, if not with 401. &lt;/li&gt;
&lt;li&gt;nginx either continues serving the file or responds with 401.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every step of this is really cheap (the db query takes about 1ms) except &lt;a href="https://en.wikipedia.org/wiki/Bcrypt"&gt;bcrypt&lt;/a&gt;. It is even specifically designed to be slow. It does hard work to be slow. That is the general idea of a good password hashing / key derivation function: you cannot take shortcuts, you have to do the hard math stuff. And while running bcrypt once during login and setting a session cookie after that is not a problem at all, running bcrypt for every file on a stateless basic auth session is actually a terrible idea. &lt;/p&gt;
&lt;h3&gt;
  
  
  Bad performance? There's Cache!
&lt;/h3&gt;

&lt;p&gt;While there might be a way to make nginx set a cookie for authenticated requests, I chose a solution that does require way less configuration and actually fixes my performance problem for most workloads. &lt;br&gt;
Nginx can cache proxy responses. All I had to do is allow caching for that response in my PHP application and add a proxy cache to the internal location block. &lt;br&gt;
The PHP code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JsonResponse&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
        &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setCache&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'max_age'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// 5min&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new nginx config like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/_remote_auth/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  
  &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;https://auth.example.com/remote_auth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
  &lt;span class="kn"&gt;proxy_pass_request_body&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
  &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Content-Length&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
  &lt;span class="kn"&gt;proxy_cache&lt;/span&gt; &lt;span class="s"&gt;auth_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
  &lt;span class="kn"&gt;proxy_cache_key&lt;/span&gt; &lt;span class="nv"&gt;$name$remote_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
  &lt;span class="kn"&gt;proxy_cache_valid&lt;/span&gt; &lt;span class="mi"&gt;5m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
  &lt;span class="kn"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This requires a proxy zone to be configured within the HTTP context in &lt;code&gt;nginx.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_cache_path&lt;/span&gt; &lt;span class="n"&gt;/var/run/nginx&lt;/span&gt; &lt;span class="s"&gt;keys_zone=auth_cache:10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;proxy_cache_key&lt;/code&gt; assures that that we cache one response per username &amp;amp; app up to five minutes. That allows us to serve all resources for that app with only one request to the actual auth backend, but we are still checking authentication for every app and every user once. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Conclusion
&lt;/h2&gt;

&lt;p&gt;I'm currently quite happy with this setup. The performance is not great but acceptable, we got basic authentication with user management being not-terrible and can extend this system pretty easy. There's an option in nginx to automatically revalidate stale caches in the background that might be useful, although the cache might not be the best solution for this performance problem. &lt;/p&gt;

&lt;p&gt;Ideas on how to improve this setup or recommendations for alternatives are always welcome!&lt;/p&gt;

&lt;p&gt;After writing all that I found (or &lt;em&gt;refound&lt;/em&gt; I'd guess) that there is some official documentation for that kind of setup from GitLab: &lt;a href="https://gitlab.com/gitlab-examples/review-apps-nginx"&gt;https://gitlab.com/gitlab-examples/review-apps-nginx&lt;/a&gt;. &lt;/p&gt;

</description>
      <category>nginx</category>
      <category>devops</category>
      <category>gitlab</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
