Saturday, November 1, 2025

Docker: Setting up Terraform, Ansible, Gitea, Nginx and Keycloak with TLS via Step-Ca

Docker: Setting up Terraform, Ansible, Gitea, Nginx and Keycloak with TLS via Step-Ca


I wanted to learn more about authentication in web apps and the Keycloak project seemed an ideal open source

way for hands-on experimentation. What followed was a docker project to include various components to achieve

this. Also as part of the setup I wanted to add Gitea for usage of IaC code repos for Ansible and Terraform. And

as part of that setup use Nginx streaming options for ssh access via Nginx. I have already decided on a template

approach to my docker projects and various links here of other setups



https://www.rjruss.info/2025/03/setting-up-sap-web-dispatcher-with-tls.html

https://www.rjruss.info/2024/11/docker-setting-up-prometheus-grafana.html

https://www.rjruss.info/2024/11/docker-setting-up-gitea-with-postgres.html


Using my docker template approach I set out for a simple Hello World type project of calling an Ansible

playbook and a Terraform configuration with a very simple Hello World authenticated web app setup. Also to

note, this is for my own home lab and not intended for wider use as step-ca offers its own way to manage

certificate renewal and calling custom scripts - but for the process I developed works for me . The authentication

is where Keycloak is required.  Reading further into Ansible and Terraform it would get way more complicated to

actually move beyond Hello World in how to control such IaC platforms via custom web calls. That didn't put me

off :) I set up both to be used independently when connecting the containers themselves. The python code was

“vibe coded” with the help of Gemini, and took a lot of troubleshooting and breaking down to individual steps to

get it to work. E.g. At one point it was luck that I was getting the right key for the tokens and debug steps helped,

but eventually I worked out how to stabilise the code with the help of Gemini. It depends on my next projects if I

want to  further vibe code the web calls, but the Keycloak knowledge was the thing I started the whole thing for

and it proved very useful.


As part of the process I eventually added Gitea to the project and set up a one off test script to push a test repo to

the Gitea host via an ssh process. With the ssh keys provided by step-ca.


The following output is the end result of building the docker compose project. (Read on for the pre-reqs before

even getting to this point).


Ansible output extract

..

PLAY [Hello World Playbook] ****************************************************


TASK [Print Hello World] *******************************************************

ok: [localhost] => {

    "msg": "Hello, World!"

}

..


Terraform output extract

Plan: 1 to add, 0 to change, 0 to destroy.


Changes to Outputs:

  + hello_world = "Hello, World! from Terraform"

null_resource.hello: Creating...

null_resource.hello: Provisioning with 'local-exec'...

null_resource.hello (local-exec): Executing: ["/bin/sh" "-c" "echo Hello, world from Terraform!"]

null_resource.hello (local-exec): Hello, world from Terraform!

null_resource.hello: Creation complete after 0s [id=4932893277560461013]


Gitea output extract


Initialized empty Git repository in /tmp/tmp.gJbulhg6Ms/testrepo/.git/

[main (root-commit) 417d75b] initial setup

 1 file changed, 1 insertion(+)

 create mode 100644 hellogitearepo.sh

Enumerating objects: 3, done.

Counting objects: 100% (3/3), done.

Writing objects: 100% (3/3), 233 bytes | 233.00 KiB/s, done.

Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)

remote: . Processing 1 references

remote: Processed 1 references in total

To ssh://gitea1.rjruss.org:44022/testuser/test.git

 * [new branch]      main -> main

branch 'main' set up to track 'origin/main'.

[SUCCESS] pushed repo to gitea

delete local temp directory and repo in gitea manually e.g.

rm -rf /tmp/tmp.gJbulhg6Ms

updated ~/.ssh/config file with following

Host gitea1.rjruss.org

    HostName gitea1.rjruss.org

    User gitea1

    IdentityFile /tmp/tmp.gJbulhg6Ms/testuser_ecdsa

    Port 44022





Read previous blogs on more of the background to my use of Docker but to highlight. Again the project

got complicated via some cyber security reasons around storing passwords in clear text and using

TLS all over the place. Some security frameworks state that any clear password is an instant

fail/breach. And personally and from security frameworks TLS is ideal in local networks.


Step 1 setup local user and directory

Creating a dedicated docker user in this example dockeruser1

        - if this is changed the .env file needs to be adapted in step 2


The directory where the downloaded project is stored can be adapted as well.

The example uses  /srv/docker-config

Example using the name of the downloaded zip as ansible-terraform-nginx-keycloak-gitea-postgres-stepca-docker.zip



sudo su - 

useradd -m -s /bin/bash -u 1245 dockeruser1

mkdir /srv/docker-config

cd /srv/docker-config


unzip {downloaded zip file} -d .


cd /srv/docker-config/ansible-terraform-nginx-keycloak-gitea-postgres-stepca-docker/

./initialGitHubScript.sh





Step 2  Adapt the .env file

Change bold entries if required and ignore any other settings in the .env file

Local docker user and group needs to match the user created in step 1



#LOCAL_SETUP - used by first_setup.sh scripts

LOCAL_DOCKER_USER=dockeruser1

LOCAL_DOCKER_GROUP=dockeruser1

LOCAL_DOCKER_VOLUME_DIR=/docker1

DOMAIN=rjruss.org

BASE_HOST=hawalma1-

VM_SHARED_ENV_GROUP=5501

...

VM_APP_STEP_PORT=9006

..

VM_PROV_NAME=baseprov

VM_AUTH_NAME=basestepCA

VM_EMAIL=generic@example.com

VM_RCOUNT=1

VM_RDAY=Sunday

#Initial certificate lifetime in hours

VM_APP_CERT_DUR=24h

#Initial expiry check in %

VM_APP_EXP_CHECK=80%

VM_ROOTC=GB

VM_ROOTCA_VALID=1825

VM_ROOTCN="Robert Russell ROOT CA"

VM_RSLEEP=2m

VM_RTIME="07:30:00"

VM_FIXED_CERT_RENEW_TIME="YES"

VM_BUFFER_CERT_HOURS=2

VM_KEYCLOAK_PROXY_PORT=44340

VM_KEYCLOAK_PROXY_HOST=keycloak1.${DOMAIN}

VM_NGINX_PORT=${VM_KEYCLOAK_PROXY_PORT}

VM_PROXY_HOST=proxy1.${DOMAIN}

VM_ANSIBLE_PROXY_HOST=ansible1.${DOMAIN}

VM_TERRAFORM_PROXY_HOST=terraform1.${DOMAIN}

VM_GITEA_PROXY_HOST=gitea1.${DOMAIN}

VM_POSTGRES_GITEA_USER=gitea

VM_GITEA_USER=testuser

VM_GITEA_EMAIL=changethis@example.com

#PROXY SSH

VM_NGINX_SSH_PORT=44022





The above would be allow the services to be accessed 

  1. Via the docker user

Adapt the dockeruser1 - group dockeruser1 is used by default

  1. Via the shared group 

Adapt the group id of 5501 a new group will be created with that gid

  1. Via a new directory/volume (it will be created if it does not exist)

Adapt the /docker1

  1. Via the following domain, hostname and ports

rjruss.org = this needs to be adapted to the required domain

hawalma1- = is the base pre-fix of all local hosts that are not part of the proxy

keycloak1.${DOMAIN} = resolved via nginx settings to point to keycloak

proxy1.${DOMAIN} = resolved via nginx settings to point to test httpbin site (for testing)

ansible1.${DOMAIN} = resolved via nginx settings to point to Ansible

terraform1.${DOMAIN} = resolved via nginx settings to point to Terraform

gitea1.${DOMAIN} = resolved via nginx settings to point to Gitea

  1. Adapt Step CA

VM_APP_STEP_PORT=9006 # Port used by Step CA

These parameters control the name of the STEP services (root certificate name)

VM_PROV_NAME=baseprov

VM_AUTH_NAME=basestepCA

  1. Adapt certificate renewal process sets the certificate renewal time, useful to set the VM_RDAY as tomorrow and VM_RTIME at an appropriate renewal time


(postgres is not open to connections other than on the docker network and uses the standard postgres port 5432

inside the docker network)


Step 3 adapt the age related password files

The age (see step 4) command is used to store passwords in an encrypted file on a docker volume. These can be

shared between containers via the bash scripts in the base docker container build.


These .*.info files should be deleted ***after completing the step 4 process***


#Postgres DB connection is certificate and password based (password can be used if postgres config adapted with

the required password setup)


*** postgress password is here but not used

*** LDAP_PW is for reference and there is a script to configure this in the Keycloak container but not used by default

*** PW in the Keycloak file is the password for the new admin user created see later screenshots and info

*** WEB_ANSIBLE & WEB_TERRAFORM are the passwords required to authenticate to run the playbook / configuration via the web call (see example test scripts below for this). 

*** PW in Step file is for the certificate generation which is password protected


./.POST.info

POSTGRES_PASSWORD="testing!123"


./.KEYCLOAK.info

PW="testing123"

LDAP_PW="testing123!"

WEB_ANSIBLE="ansible123"

WEB_TERRAFORM="terraform123"



./.STEP.info

PW="largesmallgreat!"




Step 4 Create Volumes, Networks and encrypted passwords

Following script installs operating system pre-req packages, volumes, networks and encrypts the passwords.


./first_setup.sh


After running the script the following volumes and network will be available.


 docker volume ls |grep vm

local     vm_iac_ansible_vol1

local     vm_iac_gitea_vol1

local     vm_iac_info_vol1

local     vm_iac_keycloak_vol1

local     vm_iac_keys_vol1

local     vm_iac_nginx_vol1

local     vm_iac_postgitea_vol1

local     vm_iac_postgres_vol1

local     vm_iac_step_vol1

local     vm_iac_terraform_vol1


   docker network ls |grep vm

d95e508e4dc7   vm-iac-net1   bridge    local


**** delete these password files  ****ENSURE YOU KNOW THE PASSWORDS before deleting**** 

ls -a .*.info


rm -rf .KEYCLOAK.info  .POST.info  .STEP.info  .TEST.info



Step 5 test hosts, build and run it

Network DNS resolution checks

Before building the docker compose project it is essential to check that the proxy hosts and dedicated hosts

resolve as required.


./check_hosts.sh

SUCCESSFUL checks

-   SUCCESS : hawalma1-gitea.rjruss.org resolved

-   SUCCESS : hawalma1-keycloak1.rjruss.org resolved

-   SUCCESS : hawalma1-step.rjruss.org resolved

-   SUCCESS : ansible1.rjruss.org resolved

-   SUCCESS : gitea1.rjruss.org resolved

-   SUCCESS : keycloak1.rjruss.org resolved

-   SUCCESS : proxy1.rjruss.org resolved

-   SUCCESS : terraform1.rjruss.org resolved


WARNING checks

-   WARNING : hawalma1-ansible.rjruss.org not resolving maybe not an issue as only allocated once so maybe no dependencies

-   WARNING : hawalma1-nginx.rjruss.org not resolving maybe not an issue as only allocated once so maybe no dependencies

-   WARNING : hawalma1-postgitea.rjruss.org not resolving maybe not an issue as only allocated once so maybe no dependencies

-   WARNING : hawalma1-postdb.rjruss.org not resolving maybe not an issue as only allocated once so maybe no dependencies

-   WARNING : hawalma1-terra.rjruss.org not resolving maybe not an issue as only allocated once so maybe no dependencies


ERROR checks




**SUCCESSFUL checks

The above example is from a test server to confirm this works on another server :)

The following hosts need to resolve to make this setup work


Proxy related hosts (resolved via nginx connection)


ansible1.rjruss.org resolved

keycloak1.rjruss.org resolved

proxy1.rjruss.org resolved

terraform1.rjruss.org resolved

gitea1.rjruss.org resolved


Direct related hosts that need to succeed on the resolution


hawalma1-gitea.rjruss.org resolved

hawalma1-keycloak1.rjruss.org resolved

hawalma1-step.rjruss.org resolved


***WARNING checks

The warnings are for future work or maybe not used. The hosts are part of the template based approach and for

direct connections to the containers 


su - dockeruser1

cd /srv/docker-config/ansible-terraform-nginx-keycloak-gitea-postgres-stepca-docker/

./build-it-and-run-it.sh



Could take a while depending on the server/computer performance! as it builds quite a but


Wait until you see the keycloak container logs indicate that the client-scope has completed and the health check is

UP




Step 6 Run the test Ansible and Terraform scripts 

Run “test-ansible.sh” and “test-terraform.sh” to confirm the setup is working as expected. The scripts download

the required Step based CA root certificate locally to the docker host and then calls the containers via the

nginx proxy. Take a look at the scripts for the setup.


Start a new terminal session

su - dockeruser1

cd /srv/docker-config/ansible-terraform-nginx-keycloak-gitea-postgres-stepca-docker/

#Run the Ansible test script




./test_ansible.sh

PLAY [Hello World Playbook] ****************************************************


TASK [Print Hello World] *******************************************************

ok: [localhost] => {

    "msg": "Hello, World!"

}


PLAY RECAP *********************************************************************

localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0



The script calls a python web app running on the ansible container via the nginx proxy to get the token from

Keycloak. The using the token calls Ansible again to run the playbook


A successful call is shown above with the “Hello, World!” output


#Run the Terraform test script



./test_terraform.sh


Terraform used the selected providers to generate the following execution

plan. Resource actions are indicated with the following symbols:

  + create


Terraform will perform the following actions:


  # null_resource.hello will be created

  + resource "null_resource" "hello" {

      + id = (known after apply)

    }


Plan: 1 to add, 0 to change, 0 to destroy.


Changes to Outputs:

  + hello_world = "Hello, World! from Terraform"

null_resource.hello: Creating...

null_resource.hello: Provisioning with 'local-exec'...

null_resource.hello (local-exec): Executing: ["/bin/sh" "-c" "echo Hello, world from Terraform!"]

null_resource.hello (local-exec): Hello, world from Terraform!

null_resource.hello: Creation complete after 0s [id=4932893277560461013]


Apply complete! Resources: 1 added, 0 changed, 0 destroyed.


Outputs:


hello_world = "Hello, World! from Terraform"




Using the same principal as the Ansible setup but calling the Terraform container to run the configuration. 

A successful call is shown above with the Hello, World! from Terraform output


#Run Gitea test script


This test for Gitea is a one off run to test its all working. A few changes would need to be made to the script if there

are any errors. It downloads the ssh certificate for Gitea then pushes a hellogitearepo.sh script to Gitea. It works if [SUCCESS] is output as per below


./test_gitea.sh

Successfully copied 2.05kB to /tmp/tmp.gJbulhg6Ms/.

Cannot stat /home/dockeruser1/.ssh/known_hosts: No such file or directory

# gitea1.rjruss.org:44022 SSH-2.0-Go

# gitea1.rjruss.org:44022 SSH-2.0-Go

# gitea1.rjruss.org:44022 SSH-2.0-Go

# gitea1.rjruss.org:44022 SSH-2.0-Go

# gitea1.rjruss.org:44022 SSH-2.0-Go

adding gitea1.rjruss.org to ssh config file

add, commit, add and push attempt to gitea using user testuser and email changethis@example.com

Initialized empty Git repository in /tmp/tmp.gJbulhg6Ms/testrepo/.git/

[main (root-commit) 417d75b] initial setup

 1 file changed, 1 insertion(+)

 create mode 100644 hellogitearepo.sh

Enumerating objects: 3, done.

Counting objects: 100% (3/3), done.

Writing objects: 100% (3/3), 233 bytes | 233.00 KiB/s, done.

Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)

remote: . Processing 1 references

remote: Processed 1 references in total

To ssh://gitea1.rjruss.org:44022/testuser/test.git

 * [new branch]      main -> main

branch 'main' set up to track 'origin/main'.

[SUCCESS] pushed repo to gitea

delete local temp directory and repo in gitea manually e.g.

rm -rf /tmp/tmp.gJbulhg6Ms

updated ~/.ssh/config file with following

Host gitea1.rjruss.org

    HostName gitea1.rjruss.org

    User gitea1

    IdentityFile /tmp/tmp.gJbulhg6Ms/testuser_ecdsa

    Port 44022




Certificate Renewal Control Script(s)


./sleep-control.sh display

./sleep-control.sh change_check_days_to_expire 1

./sleep-control.sh sleep_interval 2m

#below command sets the certificate check to be at 18:30 today *time should be in the future

./sleep-control.sh change  18:30 $(date +%A)

./sleep-control.sh renew_certs_days_length 1

./sleep-control.sh display



The above scripts will set the certificate renewal to the following settings


#The date and time the certificate check will take place

Wake up time to check certificate : 30-08-2025 18:30:00


#The controlling scripts will sleep every 2 minutes before checking date and time for 

# certificate renewal

Sleep interval : 2m


#Not used in this project. This is an option to check that the certificate has X days left

Days remaining for certificate expiry check : 1


#This is where Step CA command will check that the certificate still valid 

Percentage for certificate expiry check : 80%


#Length of the renewed certificate - 

Length in days a new certificate is valid for : 1





Appendix

A: General Information: Keycloak 


To access the keycloak via the web admin pages you need the Step root certificate on your host computer. A

helper script as follows is setup to cut and paste the command to get this on a windows computer via powershell.



Run,


 ./display_web_urls.sh

 ## automateit-step-run

 ## STEP powershell to trust certificate - added as a trusted root so use it on that understanding

downloading step CA certificate to /var/tmp/stepCA.pem

Successfully copied 2.56kB to /var/tmp/stepCA.pem


$B64="-----BEGIN CERTIFICATE-----

……..deleted

-----END CERTIFICATE-----"

$B64 | Out-File .\WEBSTEP.crt

Import-Certificate  -FilePath .\WEBSTEP.crt  -CertStoreLocation cert:\CurrentUser\Root


I have enclosed the powershell in the table to run on a windows computer via powershell prompt


Install step-ca root certificate on host windows 


Answer YES at the prompt

I have a homelab Hyper-V landscape with a windows DNS host, and it is important to set up dns resolution for all

the proxy hosts in this project



For my local windows based P.C I updated the host file with the proxy hosts from earlier.

My example I need the Keycloak proxy URL and Nginx Proxy port e.g. 


https://keycloak1.rjruss.org:44340/




The new admin user is hard coded as newuser the password is the PW defined in the .KEYCLOAK.info file from

earlier (that should have been deleted after noting down what the password was ;))


Keycloak Realms


I am no Keycloak expert and the realm approach is open to interpretation on how to set these up for any use case.

So my setup is a home lab with few users, therefore I decided on a dedicated realm for each use case - it is

obviously best to check out realm usage for any production based projects.


I used a naming convention to identify which Keycloak service/function was for each by noting -an- for Ansible

and -tr- for Terraform. E.g. auto-an-realm is for the public based client defined in Keycloak for the Ansible. All

the configuration is set up automatically via the kcadm.sh cli within the startup scripts for the Keycloak container.

There is no manual intervention required to run the test Hello World web apps. Check out the scripts for these

kcadm.sh commands and flow.



Screenshot shows the defined realms that are all created when starting the project for the first time. (There is a

private client realm and client defined but not currently used).




B: General Information Gitea


As part of the docker build a testuser is created for Gitea and an SSH key information is automatically uploaded

for use. SSH keys can be found at the below location once logged on as the testuser.




Miscellaneous actions


Remove demo certs from certstore, from the example code only - if the root CA name is changed then

the *basestep* match won't match any…..


Get-ChildItem -Path Cert:\CurrentUser\Root | Where-Object { $_.Subject -like '*basestep*' } | ForEach-Object {

    certutil -user -delstore Root $_.Thumbprint

}
















Google +