How to Deploy Forgejo in Kubernetes

In this short tutorial I will explain how to deploy Forgejo into a Kubernetes Cluster. Forgejo is a self-hosted lightweight source code management solution based on Git. Forgejo is a good alternative to Github if you want to host your own Git Repository server.

Forgejo was founded in 2022 as a fork of the Gitea project. In the meantime it becomes a noteable an relevant Open Source alternative to Github. The project is community driven and maintained by the Codeberg e.v. If you have an open source project you can host your project directly on codeberg.org . For private repositories you can install your own instance of Forgejo.

There are several ways how you can install Forgejo. As it is provided as a Docker Image you can easily start the system with docker compose. See the official documentation.

Read more: How to Deploy Forgejo in Kubernetes

Kubernetes

If you have a Kubernets cluster it is easy to setup a production ready deployment of Forgejo. I assume that you are familiar with Kubernetes (see also my Kubernetes Setup Project Imixs-Cloud on Gihub ) and I assume you have some kind of storage solution and ingress solution already installed.

I split the deployment into two parts, each with a separate deyployment .yaml file

  • Database – 010-postgresql.yaml
  • Git Repo – 020-forgejo.yaml

The Database

For productive environments it is recommanded to replace the build in SQLLight database with MySQL or Postgres. For a kubernetes deployment this is the recommanded way.
In the following deployment example I deploy a postgreSQL database for forgejo

---
###################################################
# Deployment PostgreSQL
###################################################
apiVersion: apps/v1
kind: Deployment
metadata:
  name: forgejo-postgres
  namespace: my-git-repo
  labels: 
    app: forgejo-postgres

spec:
  replicas: 1
  selector: 
    matchLabels:
      app: forgejo-postgres
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: forgejo-postgres
    spec:
     
      containers:
      - env:
        - name: POSTGRES_DB
          value: forgejo
        - name: POSTGRES_USER
          value: forgejo
        - name: POSTGRES_PASSWORD
          value: xxxx

        image: postgres:16-alpine
        name: forgejo-postgres

        readinessProbe:
          exec:
            command: ["pg_isready", "-U", "forgejo"]
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          exec:
            command: ["pg_isready", "-U", "forgejo"]
          initialDelaySeconds: 15
          periodSeconds: 20

          
        ports:
          - containerPort: 5432        
        volumeMounts:
        - mountPath: /var/lib/postgresql/data
          name: forgejo-dbdata
          subPath: postgres
      restartPolicy: Always
      volumes:
      - name: forgejo-dbdata
        persistentVolumeClaim:
          claimName: forgejo-dbdata


---
###################################################
# Data Volume
###################################################
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: forgejo-dbdata
  namespace: my-git-repo
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  volumeMode: Filesystem
  volumeName: "forgejo-dbdata"
  storageClassName: ""

---
kind: PersistentVolume
apiVersion: v1
metadata:
  name: forgejo-dbdata
spec:
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 10Gi
  csi:
    driver: rbd.csi.ceph.com
    fsType: ext4
    nodeStageSecretRef:
      name: csi-rbd-secret-ceph-abpua
      namespace: ceph-system
    volumeAttributes:
      "clusterID": "xxxxxxxxxxx"
      "pool": "kubernetes"
      "staticVolume": "true"
      "imageFeatures": "layering"
    volumeHandle: "forgejo-dbdata"
  persistentVolumeReclaimPolicy: Retain
  volumeMode: Filesystem


---
###################################################
# Network
###################################################
apiVersion: v1
kind: Service
metadata:
  name: forgejo-postgres
  namespace: my-git-repo
  labels: 
    app: forgejo-postgres
spec:
  clusterIP: None
  ports:
    - name: tcp
      port: 5432
  selector:
    app: forgejo-postgres

This deplyoment is quite easy and assumes that you have a storage solution (in this example a ceph cluster) to provide a data volume for the postgresql service.

The Git Repo

The second part of our deployment is the forgejo service – providing the git repo and the web applicaiton. Also here we need a data volume for the git repository and configuration and also a ingers network to access the application via HTTPS.

---
###################################################
# Deployment Forgejo
###################################################
apiVersion: apps/v1
kind: Deployment
metadata:
  name: forgejo-git
  namespace: my-git-repo
  labels: 
    app: forgejo-git
spec:
  replicas: 1
  selector: 
    matchLabels:
      app: forgejo-git
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: forgejo-git
    spec:

      containers:
      - env:
        - name: FORGEJO__security__INSTALL_LOCK
          value: "true"
        - name: FORGEJO__database__DB_TYPE
          value: postgres
        - name: FORGEJO__database__HOST
          value: forgejo-postgres:5432
        - name: FORGEJO__database__NAME
          value: forgejo
        - name: FORGEJO__database__USER
          value: forgejo
        - name: FORGEJO__database__PASSWD
          value: xxxx
        - name: FORGEJO__server__DOMAIN
          value: git.forgejo.foo.com
        - name: FORGEJO__server__ROOT_URL
          value: https://git.forgejo.foo.com/
        - name: FORGEJO__server__SSH_DOMAIN
          value: git.forgejo.foo.com
        - name: FORGEJO__service__DISABLE_REGISTRATION
          value: "true"
        # Mail
        - name: FORGEJO__mailer__ENABLED
          value: "true"
        - name: FORGEJO__mailer__PROTOCOL
          value: "smtp"
        - name: FORGEJO__mailer__SMTP_ADDR
          value: "mailgateway.my-git-repo"
        - name: FORGEJO__mailer__SMTP_PORT
          value: "25"
        - name: FORGEJO__mailer__FROM
          value: "Webmaster <webmaster@foo.com>"
        # Layout
        - name: FORGEJO____APP_NAME
          value: "Imixs Workflow"
        - name: FORGEJO____APP_SLOGAN
          value: "Git Forgejo Repository"
        - name: FORGEJO__i18n__DEFAULT_LANG
          value: "en-US"
        - name: FORGEJO__ui__DEFAULT_THEME
          value: "forgejo-dark"
          
        image: codeberg.org/forgejo/forgejo:9
        name: forgejo
        startupProbe:
          httpGet:
            path: /api/healthz
            port: 3000
          failureThreshold: 30     
          periodSeconds: 10

        readinessProbe:
          httpGet:
            path: /api/healthz
            port: 3000
          periodSeconds: 10

        livenessProbe:
          httpGet:
            path: /api/healthz
            port: 3000
          periodSeconds: 20
          failureThreshold: 3   

        ports:
          - containerPort: 3000
            name: http

        volumeMounts:
        - mountPath: /data
          name: forgejo-data
          subPath: forgejo
      restartPolicy: Always
      volumes:
      - name: forgejo-data
        persistentVolumeClaim:
          claimName: forgejo-data

---
###################################################
# Data Volume
###################################################
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: forgejo-data
  namespace: my-git-repo
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 200Gi
  volumeMode: Filesystem
  volumeName: "forgejo-repodata"
  storageClassName: ""

---
kind: PersistentVolume
apiVersion: v1
metadata:
  name: forgejo-repodata
spec:
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 200Gi
  csi:
    driver: rbd.csi.ceph.com
    fsType: ext4
    nodeStageSecretRef:
      name: csi-rbd-secret-ceph-abpua
      namespace: ceph-system
    volumeAttributes:
      # abpua
      "clusterID": "xxxxxxxxxxxxxxxx"    
      "pool": "kubernetes"
      "staticVolume": "true"
      "imageFeatures": "layering"
    volumeHandle: "forgejo-repodata"
  persistentVolumeReclaimPolicy: Retain
  volumeMode: Filesystem

---
###################################################
# Network
###################################################
apiVersion: v1
kind: Service
metadata:
  name: forgejo-git
  namespace: my-git-repo
  labels: 
    app: forgejo-git
spec:
  ports:
    - name: http
      port: 3000
  selector:
    app: forgejo-git

---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
  name: forgejo-git-tls
  namespace: my-git-repo
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    # prevent the controller from redirecting (308) to HTTPS
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
    nginx.ingress.kubernetes.io/proxy-body-size: "512m"
  
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - git.forgejo.foo.com
      secretName: tls-forgejo-git
  rules:
    - host: git.forgejo.foo.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: forgejo-git
                port:
                  number: 3000    

As you can see I added a mail configuration and my internet domain – in this example git.forgejo.foo.com. You can replace these values.

You can also adjust the other environment parameters to your needs. These are just examples.

Deplyoment

For the deployment just save the two files into a deployment directory and run:

kubectl create namespace my-git-repo
kubectl apply -f ./

As you can see from the configuraiton I disabled self-registration. To set the default admin password you can ssh the running forgejo container and run the following shell command:

su git -c "forgejo admin user create --username my-admin --password 'xxxxx' --email admin@foot.com --admin"

This will create you first admin user to login to the dashboard.

Network Access

In my deployment I use only HTTPS and disabled the SSH port 22 that is also exposed by Forgejo. But depending on your Kubernetes architecture you possible may not expose port 22. I think it is not necessary at all as the HTTPS support is very comfortable in Forgejo using API access tokens.

Conclusion

That’s it. As you can see to run Forgejo in a Kubernetes cluster is easy to achiv. If you have comments or additional ideas – let me know!

Why Not Fixing the Internet Now?

The Internet is broken. Or to be more precisely: the World Wide Web is broken. It is not representing the idea we have had in the year 1990. And I think we should fix it now. And the solution cold be very simple.

Why is it broken?

Today the Internet is full of unnecessary, unhelpful or very bad content. We all know the reasons – the commercialization and the outrageous idea of large corporations dividing the Internet into closed platforms that are called ‘social media’. As a result, it has become almost impossible today to find meaningful or valuable content on the internet. AI is currently dramatically worsen this situation day by day.

How to fix it?

So I believe it’s time to fix it. And I think this could be very simple. We can use the basic tools invented 40 years ago. The solution which I have in mind is the Proxy Server. A proxy server defines which request is valid and which one is not.

The only thing we need to organize is a Proxied-Internet based on the concepts of Mastodon .

  • Every Member of a Proxied-Internet space can register new content in the form of an URL.
  • Every Member of a Proxied-Internet space can report inappropriate content.
  • The community members can decide whether a member should be banned and no longer allowed to register new content.

Every user of the world-wide-web can simply use a Proxy in his Web-Browser to get rid of the Internet Slop.

What did you think?

Update 14.June

I have started an open source project – this is the idea in code : https://www.if1-0.org/

Running OpenTalk in Kubernetes

OpenTalk is an open-source video conferencing platform developed in Germany. This guide explains how to deploy OpenTalk v25.4.7 on a Kubernetes cluster step by step.

Component Versions

ComponentVersion
PostgreSQL16-alpine
Redis8-alpine
MinIORELEASE.2025-06-13T11-33-47Z
LiveKitv1.9.11
Controllerv0.32.11
Web Frontendv2.7.6

You can find also the release notes and the official admin guide here:

  • Release: https://docs.opentalk.eu/releases/25.4.7/
  • Admin Guide: https://docs.opentalk.eu/25.4/admin/

So let’s start…

Continue reading “Running OpenTalk in Kubernetes”

Ghost instead of WordPress – Setup a self-hosted Custom Theme

WordPress is powerful. Maybe too powerful. If all you want is a clean blog or a product landing page, you quickly find yourself fighting plugin sprawl, sluggish load times, and an admin interface designed for agencies – not for people who just want to write.

I looked around for alternatives and landed on Ghost. Open source, MIT license, modern editor, and most importantly: no overhead. Here is how I set it up.

Continue reading “Ghost instead of WordPress – Setup a self-hosted Custom Theme”

Maven – Sonatype Central Repository

Since July 2025 the old oss repositories (e.g. https://oss.sonatype.org/content/repositories/snapshots) are deprecated and are no longer accessible. If you have not yet updated your Maven Open Source project you need to change some settings. But most things are now more easy as before.

1. Create a new User Token

If you do not have yet generated a User Token, open your Sonatype Account:

https://central.sonatype.com/account

and generate a new User Token.

Next update your maven settings.xml file (e.g ~/.m2/settings.xml) with your generated username and password token:

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" 
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 
          https://maven.apache.org/xsd/settings-1.0.0.xsd">

    <servers>
      <server>
        <id>central</id>
        <username>....</username>
        <password>.............</password>
      </server>
    </servers>

</settings>

No additional settings regarding the server URLs are needed here.

Note: If you generate a new user token/password it can take a while until the new credentials are synchronized and active. So if you see the following error message this can also be an issue that the new credentials are not yet active and you need to wait a few minutes:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy (default-deploy) on project imixs-marty: Failed to deploy artifacts: Could not transfer artifact org.imixs.workflow:imixs-marty:jar:5.1.1-20250707.205537-1 from/to central (https://central.sonatype.com/repository/maven-snapshots/): authorization failed for https://central.sonatype.com/repository/maven-snapshots/org/imixs/workflow/imixs-marty/5.1.1-SNAPSHOT/imixs-marty-5.1.1-20250707.205537-1.jar, status: 403 Forbidden -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

2. Update your pom.xml File

Next update your pom.xml. The new setup is much simplified. The main issue is to replace the old nexus-staging-maven-plugin with the new central-publishing-maven-plugin.

Here is an example with the important parts:

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>.....</groupId>
	.............
	
	<repositories>
		<repository>
			<id>central-portal-snapshots</id>
			<name>Central Portal Snapshots</name>
			<url>https://central.sonatype.com/repository/maven-snapshots/</url>
			<releases>
				<enabled>false</enabled>
			</releases>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
	</repositories>

	<build>
		<plugins>

			..........

			<!-- release management -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-release-plugin</artifactId>
				<version>3.0.1</version>
				<configuration>
					<autoVersionSubmodules>true</autoVersionSubmodules>
					<useReleaseProfile>false</useReleaseProfile>
					<releaseProfiles>release</releaseProfiles>
					<goals>deploy</goals>
				</configuration>
			</plugin>

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-site-plugin</artifactId>
				<version>3.12.1</version>
			</plugin>

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-project-info-reports-plugin</artifactId>
				<version>3.1.0</version>
			</plugin>

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-javadoc-plugin</artifactId>
				<version>3.2.0</version>
				<configuration>
					<additionalOptions>-Xdoclint:none</additionalOptions>
				</configuration>
			</plugin>

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-jxr-plugin</artifactId>
				<version>3.0.0</version>
			</plugin>

		</plugins>
	</build>

	<reporting>
		<plugins>
			<!-- java doc -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-javadoc-plugin</artifactId>
				<version>3.2.0</version>
				<configuration>
					<additionalOptions>-Xdoclint:none</additionalOptions>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-jxr-plugin</artifactId>
				<version>3.0.0</version>
			</plugin>
		</plugins>
	</reporting>

	<profiles>

		<!-- Profile for Releases -->
		<profile>
			<id>release</id>
			<build>
				<plugins>
					<!-- for RELEASES: Central Publishing Plugin -->
					<plugin>
						<groupId>org.sonatype.central</groupId>
						<artifactId>central-publishing-maven-plugin</artifactId>
						<version>0.8.0</version>
						<extensions>true</extensions>
						<configuration>
							<publishingServerId>central</publishingServerId>
							<autoPublish>true</autoPublish>
							<waitUntil>published</waitUntil>
						</configuration>
					</plugin>


					<!-- Source Plugin -->
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-source-plugin</artifactId>
						<version>3.2.1</version>
						<executions>
							<execution>
								<id>attach-sources</id>
								<goals>
									<goal>jar-no-fork</goal>
								</goals>
							</execution>
						</executions>
					</plugin>

					<!-- Javadoc Plugin -->
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-javadoc-plugin</artifactId>
						<version>3.2.0</version>
						<configuration>
							<additionalOptions>-Xdoclint:none</additionalOptions>
						</configuration>
						<executions>
							<execution>
								<id>attach-javadocs</id>
								<goals>
									<goal>jar</goal>
								</goals>
							</execution>
						</executions>
					</plugin>

					<!-- GPG Signing Plugin -->
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-gpg-plugin</artifactId>
						<version>3.1.0</version>
						<executions>
							<execution>
								<id>sign-artifacts</id>
								<phase>verify</phase>
								<goals>
									<goal>sign</goal>
								</goals>
							</execution>
						</executions>
					</plugin>
				</plugins>
			</build>
		</profile>
	</profiles>


	<dependencies>
	      .............
	</dependencies>


	<!-- Distribution Management central.sonatype.org -->
	<distributionManagement>
		<snapshotRepository>
			<id>central</id>
			<url>https://central.sonatype.com/repository/maven-snapshots/</url>
		</snapshotRepository>
		<repository>
			<id>central</id>
			<url>https://central.sonatype.com</url>
		</repository>
	</distributionManagement>

</project>

I left the parts ‘url’, ‘scm’, ‘licenses’ , … here. You need to fulfill the requirements for a correct project documentation in your pom.xml

3. Deploy your Snapshot

To deploy your snapshot version just use the same command as before:

$ mvn clean deploy 

To check your snapshots you can’t any longer browse the sonatype snapshot repos with your browser. But you can request a specific snapshot URL to test if your upload was successful. E.g.:

https://central.sonatype.com/repository/maven-snapshots/org/imixs/workflow/imixs-marty/5.1.1-SNAPSHOT/maven-metadata.xml

4. Release

To release your project you can run:

$ mvn release:clean release:prepare -DautoVersionSubmodules=true
.....

$ mvn release:perform

That’s it.

How Jakarta EE and OIDC Really Works

I am developing Java Enterprise applications now for more than 20 years. Security was always an important part in developing enterprise apps. And Jakarta EE (formally known as Java EE) has always provided a perfect platform to do this. But with the new Security API in Jakarta EE 10 – especially in conjunction with OIDC, I have had a lot of problems. On the one hand, this was due to the fact that this API has developed rapidly in recent years, and only with Jakarta 10 did a truly final concept emerge.

But at the same time, this concept breaks with some established rules in enterprise development. Many aspects in Jakarta EE are perfectly abstracted through the API specification. This means that, as a developer, I do not have to think about the integration of a database cluster or how to send out emails via an SMTP gateway. I just need to develop against the API specification and finally my application will run on all Jakarta EE compatible servers. The configuration of these side aspects, such as databases or messaging, can be configured during deployment – independent from my code. This means my code dose not know anything about a vendor specific SQL Server or the Mail Infrastructure in my company. This was also true for security. It was always declarative and not part of my code base. The only thing I need to provide was for example a login page when I wanted a form based login.

OIDC is Configured By Code

Now with the new Jakarta Security 3.0 the integration of security turned into a new direction. Now your are developing a security Bean if you need specific security solutions like OIDC. Server Platforms like Wildfly or Payara offer still out of the box solutions but in all cases this means you need some kind of proprietary deployment descriptor or bind vendor specific libraries. But this often may be no option as this will break the vendor interoperability. I struggled a lot with this concept and tried to keep the connection via OIDC as configurable as possible. But in the end, I had to admit that this is not possible. The underlying concepts may be too complex to be realized abstract and interoperable.

So if you plan to secure your application with OIDC you need at least one single bean describing you security provider.

@OpenIdAuthenticationMechanismDefinition( //
                providerURI = "${oidcConfig.providerURI}", //
                clientId = "${oidcConfig.clientId}", //
                clientSecret = "${oidcConfig.clientSecret}", //
                scope = { "openid", "profile", "email", "groups" },
                claimsDefinition = @ClaimsDefinition(callerGroupsClaim = "groups", callerNameClaim = "loginname"), //
                redirectURI = "${oidcConfig.redirectURI}")
public class SecurityConfig {

As you can see in this example the bean does not have any methods and is just declaring one single annotation. So we can say this is our configuration. As you can see I am using a configuration bean instead of hard coding things like the client secret. This configuration bean is again using the Jakarata EE / Micropfofile API to allow a flexible configuration via config files or environment variables:


import java.io.Serializable;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;


@ApplicationScoped
@Named
public class OidcConfig implements Serializable {
    @Inject
    @ConfigProperty(name = "OIDCCONFIG_PROVIDERURI", defaultValue = "http://localhost/")
    String providerURI;

    @Inject
    @ConfigProperty(name = "OIDCCONFIG_CLIENTID", defaultValue = "undefined")
    String clientId;

    @Inject
    @ConfigProperty(name = "OIDCCONFIG_CLIENTSECRET", defaultValue = "undefined")
    String clientSecret;

    @Inject
    @ConfigProperty(name = "OIDCCONFIG_REDIRECTURI", defaultValue = "undefined")
    String redirectURI;

    public String getClientId() {
        return clientId;
    }

    public String getClientSecret() {
        return clientSecret;
    }

    public String getProviderURI() {
        return providerURI;
    }

    public String getRedirectURI() {
        return redirectURI;
    }

}

So at the end this all works fine and I can configure things like the clientID or the clientSecret at deploy time. But my point is that you cannot avoid using this kind of implementation.

As you also can see in my first example, it is using some additional parameters like the ‘scope‘ or the ‘claimsDefinition‘ that are mostly tightly coupled to the Open ID Provider you use. And this may break the interoperability of you code.

Working With OIDC Modules

At the end we need to accept that the Security API in Jakarta EE is what it is. Unfortunately, in the area of SSO and OIDC, there are many different providers that often impose very vendor specific configurations. Therefore, my recommendation here is to place the Security Bean for the OIDC connection always into a separate module (jar library). This allows you to implement different variants of the security bean. So as a result you project can look like this:

.
├── my-app
│   └── src
├── my-app-odic-keycloak
│   ├── pom.xml
│   ├── src
│       └── main
│           └── java
│               └── com
│                   └── SecurityConfig.java
├── my-app-odic-auth0
│   ├── pom.xml
│   ├── src
│       └── main
│           └── java
│               └── com
│                   └── SecurityConfig.java
......

This allows you to implement vendor specific configurations if needed. And you can decide on build time which of your OIDC jars you link to your final deployment. This gives you more flexibility and your application code is still not bound to one single OIDC configuration. You can even fall back to the default Jakarta EE security modules if required.

Conclusion

Perhaps I am being a bit pedantic here. But as a Jakarta EE developer, one is used to developing strictly against an API. For a Spring project, my considerations may seem exaggerated. However, when it comes to developing truly interoperable applications, these considerations are definitely justified. I look forward to your feedback.

Update 25. June 2025

We have developed a new OIDC Module that allows an easy and platform independent integration of ODIC into modern Web Applications.
Find details about Imixs-Security-OIDC here.

Wildfly 29 – OIDC – Bearer Token Authentication

In this blog post I explain the setup of a application running on Wildfly 29 using the OIDC authentication mechanism. It took me a long time to figure out the correct and necessary configuration steps. My requirement was not only to authenticate a user with Keycloak via OpenID Connect (OIDC), but also enable my backend services to authenticate programmatically to access the Rest API.

So we have two requirements: User login via Keycloak/OIDC and a programmatically login for backend service. The later is called Bearer Authentication mechanism.

Bearer Token Authentication

The Bearer Token Authorization is the process of authorizing HTTP requests through a valid Bearer Token. Such a token can be obtained from a Identity Authority like Keycloak using a simple curl command. For example to get a valid token from a Keycloak server you can run:

curl -X POST \
  -d "grant_type=password" \
  -d "client_id=imixs" \
  -d "client_secret=xxxxxxxxxxxxxxxxxxx" \
  -d "username=anna" \
  -d "password=123" \
  "https://my-keycloak.server/realms/my-keycloak-realm/protocol/openid-connect/token"

This will result in a JSON Web Token (JWT) containing differnet sections.

{"access_token":"eyxxxxx.eyxxxxxxxxx",
  "expires_in":300,
  "refresh_expires_in":1800,
  "refresh_token":"eyyyyyyyyyy.eyyyyyyy",
  "token_type":"Bearer",
  "not-before-policy":0,
  "session_state":"fc2f7e36-e4ba-145a-b493-efb287ec0c7a",
  "scope":"profile email"
}

The interesting one is the ‘access_token’. You can copy this part and now you can request a secured resource from your applications Rest API:

curl -X GET \
  -H "Authorization: Bearer eyyyyyyyyyyyyyyyyyy" \
  "https://my-app/api/documents/ABC"

OK, this all sounds very easy and straight forward. But due to the fact that this security mechanisms evolving fast also in wildfly there were differnet concepts used in the past. So the following will work for Wildfly 29 (and hopefully later) version.

The Wildfly Descriptor ‘oidc.json’

An easy and very fast setup is to use the Wildfly specific deployment descriptor file ‘oidc.json‘. This file is placed in /WEB-INF/ directory:

{
    "client-id" : "my-client-id",
    "provider-url" : "https://my-keycloak.server/realms/my-keycloak-realm",    
    "principal-attribute" : "preferred_username",  
    "credentials" : {
        "secret" : "xxxxxxxxxxxxxxx"
    }
}

In addition change the login-config in your web.xml file to ‘OIDC’

...
  <login-config>
    <auth-method>OIDC</auth-method>
  </login-config>
...

No further configuration is needed. No realms need to be configured at all in the standalone.xml or in your application.

The Jakarta OpenIdAuthenticationMechanismDefinition

Jakarta EE 10 includes a new authentication mechanism: OpenID Connect! This can be added to a Jakarta EE servlet using the new @OpenIdAuthenticationMechanismDefinition annotation.

This annotation is the standarized way to use OIDC authentication mechanism. You need to implement a CDI security bean in your application like shown in the following example:

import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.security.enterprise.authentication.mechanism.http.OpenIdAuthenticationMechanismDefinition;
import jakarta.security.enterprise.identitystore.openid.AccessToken;
import jakarta.security.enterprise.identitystore.openid.OpenIdContext;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@RequestScoped
@Path("/oidc")
@Produces({ MediaType.TEXT_PLAIN })
@OpenIdAuthenticationMechanismDefinition( //
        clientId = "${oidcConfig.clientId}", //
        clientSecret = "${oidcConfig.clientSecret}", //
        redirectURI = "${baseURL}/callback", //
        providerURI = "${oidcConfig.issuerUri}" //
)
public class Securitybean implements Serializable {
    private static final long serialVersionUID = 1L;
   
    @Inject
    Principal principal;

    @Inject
    private OpenIdContext context;

    @GET
    @Produces("text/plain")
    public String sessionInfoAuth() {
        String message = "";
        try {
            System.out.println("=========================================");
            if (principal != null) {
                System.out.println("  Principal name: " + principal.getName());
            } else {
                System.out.println("  Principal resolved to null!");
            }
            // Here's the unique subject identifier within the issuer
            if (context == null) {
                message = "Failed to resolve OpenIdContext!";
            } else {
                System.out.println("  Subject = " + context.getSubject());
                System.out.println("  Access token = " + context.getAccessToken());
                System.out.println("  ID token = " + context.getIdentityToken());
                System.out.println("  Claims json = " + context.getClaimsJson());
                System.out.println("=========================================");
                message = "Imixs-Security-OIDC ==> OK \n" + //
                        "User Principal      ==> " + principal.getName()
                        + "\n\nSession details are available on server log";
            }
        } catch (Exception e) {
            message = "Failed to resolve OpenIdContext!";
        }
        return message;
    }
}

The important part is only the annotation. I added the method sessionInfoAuth only for convenience to provide a rest API to check the auth information.

Using this mechanism it is important to disable the integrated-jaspi module in your standalone.xml file:

...
        <subsystem xmlns="urn:jboss:domain:undertow:14.0" default-virtual-host="default-host"
            default-servlet-container="default" default-server="default-server"
            statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}"
            default-security-domain="other">
            ......
            <application-security-domains>
                <application-security-domain name="other" security-domain="ApplicationDomain"
                    integrated-jaspi="false" />
            </application-security-domains>
            .......
        </subsystem>            
.....  

The problem is, that with this setup you can login as a user like before with the oidc.yaml file, but a programmatic login with the access token is no longer possible.

If you find an solution for this problem, please let me know 😉

Update 25. June 2025

I have developed a new OIDC Module that allows an easy and platform independent integration of ODIC into modern Web Applications.
Find details about Imixs-Security-OIDC here.

PostgreSQL: Major Upgrade in Kubernetes

In the following I show an example how you can upgrade an old PostgreSQL server to a new Major version running in a Kubernetes cluster. In this example I upgrade directly from 9.6.1 to 17.4. My deployment runs on Kubernetes and I have external data volumes bound to my servers based on a ceph system. The migration concept in short is the following:

  1. Mount a new /backup/ volume to backup the data on the old databasesever
  2. Backup the existing database with pg_dump
  3. Undeploy your old PostgreSQL Server
  4. Create a new deployment for the new empty Server and mount the /backup/ volume
  5. Restore the backup with pg_restore
Continue reading “PostgreSQL: Major Upgrade in Kubernetes”

EJB => CDI Migration

In this blog post I will try to explain how to replace Jakrata EE EJBs with CDI beans. In onw of the future releases of Jakarta EE (possible version 12) the EJB concepts will be fully replaced by CDI technology. The reason simply is that EJBs become more and more outdated as the technology is based on older concepts that today are no longer recommended. Another goal for the replacement is to make developers life easier and not providing two very similar technologies in parallel. The Imixs-Workflow project is fully based on Jakarta EE and we are using also EJBs in some of its core components. So this will also be a kind of travel guide of my own journey from EJB to CDI.

The Basics

So first question: Why will EJBs be removed? The first and most obvious answer is: it does not make sens for the Jakarta EE project to support tow similar technologies in parallel. CDI is the newer technology and already today provides a lot of concepts from EJBs. So often in a Jakrata EE project you can either choose to implement a Service in a EJB or CDI bean without any difference in its result.

One of the more hidden reasons is that EJBs were invented at a time when the Java VM did not yet offer the performance and functionality that it does today. At that time, it was simply not efficiently possible to use a bean instance in a multi-threaded situation without running into a problem with the VMs garbage collector that it could no longer keep up cleaning old objects. The was the reason for the EJB Container and its pooling mechanism. That means in EJB a client always gets an EJB instance exclusive and can use it in a thread save way. If all EJBs from the pool are in use a new client request have to wait until one of the pools EJB instances is free again. This was and is a very robust and thread save mechanism and makes the developers life very easy. In a CDI Container we don’t have this kind of pooling and so the first result is the different code layout of CID implementations.

An EJB implementation typical looks like this:

package com.example;

import jakarta.ejb.EJB;
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

@Stateless
public class StatelessBeanInEJB {

  @PersistenceContext
  private EntityManager entityManager;

  // The @TransactionAttribute(TransactionAttributeType.REQUIRED) // annotation is optional; this is the default already.
  public void transactionalMethod() {
   // ...
  }


  @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
  public void independentTransactionalMethod() {
   // ...
  }


}

Now this is how the same looks in CDI with help of the in Jakarta Transactions 2.0:

package com.example;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.transaction.Transactional;
import jakarta.transaction.Transactional.TxType;

@ApplicationScoped
public class StatelessBeanInCDI {

  @PersistenceContext
  private EntityManager entityManager;

  @Transactional // The annotation value TxType.REQUIRED is optional; this is the default already.
  public void transactionalMethod() {
   // ...
  }

  @Transactional(TxType.REQUIRES_NEW)
  public void independentTransactionalMethod() {
   // ...
  }


}

The CDI bean has been marked @ApplicationScoped and is no longer pooled. And the CDI instances are unsynchronized while EJB instances are synchronized.

Synchronized vs Unsynchronized – Was does this mean?

I’ll explain the important difference between synchronized EJB instances and non-synchronized CDI instances:

EJB (@Stateless) – synchronized:

  • With EJBs, each bean instance from the pool is only used by one thread at a time
  • The container automatically ensures this thread safety
  • If several threads want to access the bean at the same time, they have to fetch a free instance from the pool or wait

This makes implementation easier because you don’t have to worry about thread safety.
However, it can lead to performance degradation under high load because threads have to wait.

CDI (@ApplicationScoped) – unsynchronized:

  • A CDI Bean instance can be used by multiple threads in parallel
  • There is no automatic synchronization by the container
  • The developer is responsible for thread safety

This allows for better performance under high load, as no threads have to wait.
However this requires a more careful implementation to avoid race conditions.

Here is an example:

@ApplicationScoped
public class UnsynchronizedCounter {
    private int count = 0; // shared state

  // NOT thread-save!
  public void increment() {
  count++;  // can lead into a Race Condition
  }

  // Thread-save Version
  public synchronized void incrementThreadSafe() {
   count++;
  }
}

So with CDI, we have to pay attention to thread safety ourselves if the bean has shared state. Possible solutions are:

  • Using Synchronized Methods/Blocks
  • Use thread-safe data structures (e.g. AtomicInteger)
  • Working stateless
  • Use a narrower scope like @RequestScoped

The EJB version would automatically be thread-safe, but less performant under high load.

Using instance variables in stateless EJBs was always a very bad practice but is was possible. So if you have clean implementations of EJBs without using instance variables, on the first glance it should be easy to transfere your EJB into a CID bean by just replacing the annotation @Stateless with @ApplicationScoped.

But now let’s take a deeper look into the details….

… will be continued ….