2019年11月5日 星期二

[ASP.NET Core] Identity Server 4 – Dockerize


 ASP.NET Core   Identity Server 4   Docker  


Introduction



We are going to dockerize the services to containers so far, including of

l   Backend Server
l   Auth Server (Idsrv4)
l   Redis
l   OpenLdap
l   Nginx (Optional)

This article won’t show details of how to write dockerfile or docker-compose file but only tips and traps (lol).
If you would like to get started with Docker, you can take a look at my Docker ebook : )

Environment


Docker 18.05.0-ce
ASP.NET Core 3.0.100



Implement



The source code is on my Github.


Architecture

The services’ architecture after dockerized is as following. Each block presents a container.




My project’s directories and files:

Directory/File
Description
/docker
/build
/auth
Auth Server’s build binaries
/Backend
Backend’s build binaries
/certs
Docker.crt

Self-Signed certificate by OpenSSL
Docker.key
Docker.pfx
.env

Environment file for Compose
auth.dockerfile

Dockerfile for Auth Server
backend.dockerfile

Dockerfile for Backend Server
docker-compose.yml

Docker Compose file
/src


Source code


Build

I build the project at local without using a dotnet-core build container.


Auth Server

$ cd src/AspNetCore.IdentityServer4.Auth
$ dotnet publish --output ../../docker/build/auth --configuration release


Backend

$ cd src/AspNetCore.IdentityServer4.WebApi
$ dotnet publish --output ../../docker/build/backend --configuration release



Self-signed Certificate

Use the following commands to create certificate. Notice that we only need PFX for Kestrel (Auth/Backend Server) so far. The CRT and KEY will be used on Nginx later, so do not delete them.

$ cd docker/certs
$ openssl req -newkey rsa:4096 -nodes -sha256 \
-keyout Docker.key -x509 -days 3650 \
-out Docker.crt
$ openssl pkcs12 -export -out certs/jb.pfx -inkey Docker.key -in Docker.crt


See [ASP.Net Core] Self-signed SSL certificate for how to use OpenSSL.


Dockerfile

Here is the Dockerfile for Backend,

FROM mcr.microsoft.com/dotnet/core/aspnet:3.0 AS runtime

# Arg for current environment, eg. Development, Docker, Production. Use docker-compose file to overwrite.
ARG env="Docker"

WORKDIR /app
VOLUME /app/App_Data/Logs

RUN mkdir -p /etc/docker/certs/ 
COPY ./certs/${env}.pfx /etc/docker/certs/
RUN mv /etc/docker/certs/${env}.pfx /etc/docker/certs/docker.pfx
COPY ./build/backend ./

ENV TZ "Asia/Taipei"
ENV ASPNETCORE_URLS "http://+:5000;https://+:5001"
ENV ASPNETCORE_ENVIRONMENT ${env}
ENV ASPNETCORE_Kestrel__Certificates__Default__Password ""
ENV ASPNETCORE_Kestrel__Certificates__Default__Path "/etc/docker/certs/docker.pfx"
EXPOSE 5000 5001

ENTRYPOINT ["dotnet""AspNetCore.IdentityServer4.WebApi.dll"]


Key points:

l  The argument (ARG): env, is set as “Docker” in default. However, we can overwrite it in Compose file with the environment variable from .env file => we will see it later.

l  Copy the PFX into container and rename it as docker.pfx.

l  Set Kestrel’s listening ports by environment variable: ASPNETCORE_URLS.

l  Set Kestrel’s SSL Certificate by environment variables:
n   ASPNETCORE_Kestrel__Certificates__Default__Password
n   ASPNETCORE_Kestrel__Certificates__Default__Path



Environment File for Compose (.env)

The Environment file specifies the values of COMPOSE_PROJECT_NAME, Ports and DOCKER_ENV.
Notice that the custom environment variable: DOCKER_ENV, will be set to ASPNETCORE_ENVIRONMENT for Kestrel and copy the mapping Certificate (e.q. Docker.crt) to container.

.env

COMPOSE_PROJECT_NAME=idsrv4
DOCKER_ENV=Docker
LDAP_PORT=389
REDIS_PORT=6379
BACKEND_PORT=5000
BACKEND_HTTPS_PORT=5001
AUTH_PORT=6000
AUTH_HTTPS_PORT=6001



Compose file

Now we will use Docker Compose to run the services:

l   Backend Server
l   Auth Server (Idsrv4)
l   Redis
l   OpenLdap



Notice that we have to mount OpenLdap’s database and configuration from
-   /var/lib/ldap/
-   /etc/ldap/slapd.d
to host in order to keeping the LDAP entities/settings.

For Redis, mount /data out.


                                                         

docker-compose.yml

version"3"
services:
  
  openldap:
    imageosixia/openldap:stable
    container_nameidsrv-openldap
    volumes:
      - openldap_database:/var/lib/ldap
      - openldap_slapd:/etc/ldap/slapd.d
    ports:
      - ${LDAP_PORT}:389
    expose:
      - 389

  redis:
    imageredis:latest
    container_nameidsrv-redis
    volumes:
      - redis_data:/data
    ports:
       - ${REDIS_PORT}:6379
    expose
       - 6379

  auth:
    imageidsrv4-auth:latest
    build:
      context.
      dockerfileauth.dockerfile
      args:
        env${DOCKER_ENV}
    container_nameidsrv-auth
    networks
      - default
    ports:
      - ${AUTH_PORT}:6000
      - ${AUTH_HTTPS_PORT}:6001
    volumes:
      - ../Logs/Auth:/App_Data/Logs
    depends_on:
      - openldap
      - redis

  backend:
    imageidsrv4-backend:latest
    build:
      context.
      dockerfilebackend.dockerfile
      args:
        env${DOCKER_ENV}
    container_nameidsrv-backend
    networks
      - default
    ports:
      - ${BACKEND_PORT}:5000
      - ${BACKEND_HTTPS_PORT}:5001
    volumes:
      - ../Logs/Backend:/App_Data/Logs
    depends_on:
      - openldap
      - redis

networks:
  default:
    driverbridge

volumes:
  openldap_database:
  openldap_slapd:
  redis_data:


Now we can build Docker images and run the services by Compose file,

$ cd docker
$ docker-compose build [--no-cache]
$ docker-compose up -d




Here are the running containers with the Compose file…







(optional) Architecture with Nginx

We can put a Nginx Reverse Proxy container to proxy the web server(s). For example,




My project’s directories and files will be updated as following:

Directory/File
Description
/docker
/build
/auth
Auth Server’s build binaries
/Backend
Backend’s build binaries
/certs
Docker.crt

Self-Signed certificate by OpenSSL
Docker.key
Docker.pfx
/nginx
web-servers.conf
Nginx config for sites-enabled
.env

Environment file for Compose
auth.dockerfile

Dockerfile for Auth Server
backend.dockerfile

Dockerfile for Backend Server
nginx.dockerfile

Dockerfile for Nginx
docker-compose.yml

Docker Compose file
/src


Source code



web-servers.conf

Nginx will need CRT and KEY that we created in the first step for setting SSL Certificate.


# Internal host(s)
upstream backend {
  server backend:5001;
}

ssl on;
ssl_certificate           /etc/docker/certs/docker.crt;
ssl_certificate_key       /etc/docker/certs/docker.key;

# Proxy backend server listens on port 5000,5001
server {
    listen 5000;
    listen 5001 ssl;

    location / {
        proxy_set_header      Host $http_host;
        proxy_set_header      X-Real-IP           \$remote_addr;        
proxy_set_header      X-Forwarded-For     \$proxy_add_x_forwarded_for;
        proxy_set_header      X-Forwarded-Proto   \$scheme;
        proxy_read_timeout                        600s;
        proxy_pass             https://backend;
    }
}



Dockerfile: nginx.dockerfile

FROM nginx:1.17.5

ARG env="Docker"

COPY ./nginx/web-servers.conf /etc/nginx/sites-available/web-servers.conf
COPY ./certs/${env}.crt /etc/docker/certs/
COPY ./certs/${env}.key /etc/docker/certs/
RUN mv /etc/docker/certs/${env}.crt /etc/docker/certs/docker.crt
RUN mv /etc/docker/certs/${env}.key /etc/docker/certs/docker.key
RUN ln -s /etc/nginx/sites-available/web-servers.conf  /etc/nginx/conf.d/web-servers.conf

ENV TZ "Asia/Taipei"

EXPOSE 80 5000 5001


Compose file (Updated)

Update the Compose file by adding Nginx service and remove expose ports from Backend Service.
The below YML file only shows the updated part:

version"3"
services:
  
  # ...skip openldap, redis, auth 

  backend:
    imageidsrv4-backend:latest
    build:
      context.
      dockerfilebackend.dockerfile
      args:
        env${DOCKER_ENV}
    container_nameidsrv-backend
    networks
      - default
    # ports:
    #   - ${BACKEND_PORT}:5000
    #   - ${BACKEND_HTTPS_PORT}:5001
    volumes:
      - ../Logs/Backend:/App_Data/Logs
    depends_on:
      - openldap
      - redis

  nginx:
    imageidsrv4-nginx:latest
    container_nameidsrv-nginx
    build:
      context.
      dockerfilenginx.dockerfile
      args:
        env${DOCKER_ENV}
    networks
      - default
    ports:
      - 80:80
      - ${BACKEND_PORT}:5000
      - ${BACKEND_HTTPS_PORT}:5001
    depends_on:
      - auth
      - backend

# ... skip networks, volumes configuration



Here are the running containers with the Compose file…






Wait a minute! Where re the TRAPs?

Yeah, the most important part is the TRAPS!


SSL Connection cannot be established from Backend to Auth Server

While we used Self-signed certificate, the following connection problem had a great chance to occur!

{
    "error": "Error connecting to https://auth:6001/.well-known/openid-configuration. The SSL connection could not be established, see inner exception.."
}


Method 1.

To make the HttpClient on Backend connect to Auth Server’s API with untrusted certificate, we can define the HttpClientHandler NOT to validate the server certificate.

For example,

services.AddHttpClient("AuthHttpClient")
     .ConfigurePrimaryHttpMessageHandler(h =>
      {
        var handler = new HttpClientHandler();
        handler.ServerCertificateCustomValidationCallback = 
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
        return handler;
      });



Method 2.

The other way is to trust the Self-signed certificate on Backend Server.

First install ca-certificates,

$ apt-get install ca-certificates


Now we can include the certificate,

$ cp /etc/docker/$CERT /usr/share/ca-certificates
$ dpkg-reconfigure ca-certificates


Choose option “3. ask” and select the Self-signed certificate.




Then select the Self-signed certificate, (e.q. Certificates to activate: 1)




Which will result in




Last step, we need to tell update-ca-certificates explicitly to activate the cert by adding it to
/etc/ca-certificate.conf or /etc/ca-certificate/update.d
and copy the certificate to
/usr/local/share/ca-certificates

$ CERT=Docker.crt
$ echo "+$CERT" >/etc/ca-certificates/update.d/activate_my_cert
$ cp /etc/docker/$CERT /usr/local/share/ca-certificates/
$ update-ca-certificates

Results:



It’s done but there is one issue left:


Notice that we use the container name: “auth”, in Backend’s appsettings as the Auth Server’s base url:

"Host": {
    "AuthServer""https://auth:6001",
}

So the Self-signed certificate’s CN must matches it or the SSL connection will still be failed!
Take curl’s message for example,


Change the base url to 192.168.99.100 or use the right CN can solve this issue.







Source Code






Reference








沒有留言:

張貼留言