Posted on Leave a comment

Integrate Keycloak With Spring Boot Step by Step

Recently, I had to configure an API to use Keycloak to authenticate and authorize users of a web application. After spending almost two weeks, I finally got the app up and running as required. If you are on the same boat, read on since I’ll provide step by step (with pictures!) to help you save time on this setup so you can spend more time with actual development.

API Requirements

There are some requirements as follow:

  • The spring boot API can create and login (return access token) to the caller
  • There are three roles (admin, moderator, member) in the app and each API endpoint can be guarded by a specific role require. There are also endpoints that don’t require authorization (publicly available)
  • Endpoints that are accessible by member are also accessible by admin and moderator. Endpoints that are accessible by moderator are accessible by admin

Code repository

If you are the impatient kind (like me), you can skip the post and go directly to the source code (available on Github) to start tinkering with the application here.

However, when you are stuck, feel free to go back to this post to find the missing pieces.

Quick Keycloak setup with docker compose

If you have a Keycloak instance up and running, you can skip this part. However, in case you haven’t, use the docker-compose file below to quickly set up Keycloak:

version: '3'

services:
  keycloak:
    container_name: keycloak
    image: jboss/keycloak:15.0.2
    restart: always
    env_file: ./keycloak.env
    depends_on:
      - keycloak_db
    volumes:
      - ./realm.json:/tmp/realm.json
    ports:
      - "18080:8080"


  keycloak_db:
    container_name: keycloak_db
    image: mariadb:10.3.26
    restart: always
    volumes:
      - keycloak_db_vol:/var/lib/mysql
    env_file:
      - ./keycloak.env
      
volumes:
  keycloak_db_vol:      

The keycloak.env contains some environment variables:

KEYCLOAK_USER=kc_dev
KEYCLOAK_PASSWORD=kc_dev1231232
KEYCLOAK_IMPORT="-Dkeycloak.profile.feature.upload_scripts=enabled"
DB_VENDOR=mariadb
DB_ADDR=keycloak_db:3306
DB_DATABASE=keycloak_1
DB_USER=root
DB_PASSWORD=root

MYSQL_ROOT_PASSWORD=root
MARIADB_DATABASE=keycloak_1

Now run docker-compose up -d then you should be able to access keycloak at http://localhost:18080 in a few minutes (docker may need to pull the images from its registry, which depends largely on the speed of your connection).

For the lazy guys, please go to this location in the repo and hit docker-compose up -d

Once, you have access to Keycloak, login with the id and password specified in the environment file (kc_dev and kc_dev1231232).

In the beginning, there should be only one realm, you need to click on Add realm to create a new realm.

Create a new realm in keycloak
Create a new realm in keycloak

Now the keycloak instance is ready. It’s time to create and configure a client.

Keycloak client configuration

The client’s configuration is very important so you need to pay close attention to this part.

At the beginning, there are some default clients:

Keycloak's default clients

You can click on the Create button on the right to create a new client like this:

Create new keycloak client

And configure the client like this:

keycloak client's configuration

Now, it’s time to create roles for the client. As you can remember, we have three roles:

  • admin
  • moderator
  • member

From the requirement at the beginning, we know that admin and moderator are composite roles while member is not a composite role.

A composite role is a role that consists of one or
more other roles

Let’s switch to the Roles tab and click on Add role

add new role in keycloak

After creating the role, you will see there is a switch to turn that role into a composite role. For the member role, we don’t need that.

However, as mentioned above, admin and moderator are composite roles, we need to turn that option on:

adding composite role

As you can see in this case, when creating moderator role, I turned the composite role on. In the Client roles select box, I select this client (spring-boot-client).

Let’s do the same for the admin role.

You don’t need to add the member role to the associated roles box since moderator includes member already.

Spring boot application configuration

Let’s configure the application.properties file.

#keycloak
keycloak.realm=datmt-test-realm
keycloak.auth-server-url=http://localhost:18080/auth/
#use external in production
keycloak.ssl-required=none

#name of the client
keycloak.resource=spring-boot-client
keycloak.credentials.secret=ee923b32-a4f1-4435-8066-9eb4541e8165
keycloak.use-resource-role-mappings=true
keycloak.bearer-only=true

All the fields in this configuration file are self-explanatory. You may wonder where to get the secret? It’s in the Credentials tab of the client:

getting keycloak's client secrets

Also, for Keycloak to guard your endpoints, you need to create a configuration file like this:

package com.datmt.keycloak.springbootauth.config;

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests()
            .antMatchers("/public/**").permitAll()
            .antMatchers("/member/**").hasAnyRole("member")
            .antMatchers("/moderator/**").hasAnyRole("moderator")
            .antMatchers("/admin/**").hasAnyRole("admin")
            .anyRequest()
            .permitAll();
        http.csrf().disable();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Bean
    public KeycloakConfigResolver KeycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }
}

Here, you can see that I’ve configured that:

  • Paths begins with /admin requrie admin role
  • Paths begin with /member require member role
  • Paths begin with /moderator require moderator role
  • Paths begin with /public is accessible by all

Let’s create a controller and test our config:

package com.datmt.keycloak.springbootauth.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.security.RolesAllowed;

@RestController
public class HelloController {

	@GetMapping("/public/hello")
	public ResponseEntity<String> helloPublic() {
		return ResponseEntity.ok("Hello public user");
	}
	
	@GetMapping("/member/hello")
	public ResponseEntity<String> helloMember() {
		return ResponseEntity.ok("Hello dear member");
	}
	
	@GetMapping("/moderator/hello")
	public ResponseEntity<String> helloModerator() {
		return ResponseEntity.ok("Hello Moderator");
	}
	
	
	@GetMapping("/admin/hello")
	public ResponseEntity<String> helloAdmin() {
		return ResponseEntity.ok("Nice day, admin");
	}

	
}

Let’s try the public endpoint:

unguarded endpoint
Public endpoint works

As you can see, it works as expected. The path begins with /public so anyone can access without any authentication.

Now let’s try the path for member:

and also admin:

You can see that since the request didn’t present any kind of credentials, the response is 401.

To make sure this works, let’s create users with specific roles and test if they can access the endpoints with his token.

Let’s first create an user with member role:

After creating the user, go to Role Mappings tab, select spring-boot-client and add member to user’s list of roles

Now, to quickly get the token for this user, go back to the details tab and click on Impersonate:

In the new tab that appears, open the network tab of the developer console, reload and copy the access token:

impersonate and get token

Now, use that token to make a request in postman:

You expected that to be a 200 response, right? But what I got was 403. That’s better than 401 since we can authenticate with the token but somehow, the user was not authorized to access the endpoint.

What’s the missing configuration?

If you decode the token using sites like jwt.io, you will see that even though the user has role member, it didn’t show in the token:

It turned out, you also need to configure client scopes so the roles appear on the token.

Then go to Scopes and add all roles of the client spring-boot-client

Now, let’s impersonate the user again. Now, her roles should be visible on the token decoding page:

Let’s make the same request to them member’s endpoint again but with the new token, sure enough, it worked:

You can try with other roles, they should work too.

Guarding API with RolesAllowed

Guarding endpoint with path prefixes is OK and sometimes that’s a clean solution. However, there are times you may need a finer tune to your access policy.

In such case, you can use RolesAllowed to specify which role can access a specific endpoint, no matter what its path prefix is.

	@RolesAllowed("member")
	@GetMapping("/other/hello")
	public ResponseEntity<String> helloCustom() {
		return ResponseEntity.ok("Nice day, my custom user");
	}

You can guess from the annotation, anyone with role member can access this endpoint.

What if the path is prefix but /public?

	@RolesAllowed("member")
	@GetMapping("/public/hello-fake-public")
	public ResponseEntity<String> helloCustom() {
		return ResponseEntity.ok("Nice day, it appears to be public but not");
	}

If you try to access the URL, even though the prefix is public, you will get a 401 without a token:

But if a user with member role, the access is granted:

overwrite path prefix with rolesallowed

Create account and login using Keycloak Admin Client

Now the API can talk to keycloak to authenticate and authorize users, let’s also integrate user login and creation to the API so we can get the token via the API.

Before doing anything with the code, let’s add some more configuration to the client so it has what it needs to manage user.

First, go to the client’s Service Account Role tab and configure as follow:

Configure role for the service account
Add manager users, query users role for the client’s service account

Now, the client is able to manage users.

Let’s add the keycloak-admin-client package to maven. You need this to enable spring boot to operate Keycloak’s admin-related tasks.

		<dependency>
			<groupId>org.keycloak</groupId>
			<artifactId>keycloak-admin-client</artifactId>
			<version>${keycloak.version}</version>
		</dependency>

Next add a KeycloakProvider class:

@Configuration
@Getter
public class KeycloakProvider {

    @Value("${keycloak.auth-server-url}")
    public String serverURL;
    @Value("${keycloak.realm}")
    public String realm;
    @Value("${keycloak.resource}")
    public String clientID;
    @Value("${keycloak.credentials.secret}")
    public String clientSecret;

    private static Keycloak keycloak = null;

    public KeycloakProvider() {
    }

    public Keycloak getInstance() {
        if (keycloak == null) {

            return KeycloakBuilder.builder()
                    .realm(realm)
                    .serverUrl(serverURL)
                    .clientId(clientID)
                    .clientSecret(clientSecret)
                    .grantType(OAuth2Constants.CLIENT_CREDENTIALS)
                    .build();
        }
        return keycloak;
    }


    public KeycloakBuilder newKeycloakBuilderWithPasswordCredentials(String username, String password) {
        return KeycloakBuilder.builder() //
                .realm(realm) //
                .serverUrl(serverURL)//
                .clientId(clientID) //
                .clientSecret(clientSecret) //
                .username(username) //
                .password(password);
    }

    public JsonNode refreshToken(String refreshToken) throws UnirestException {
        String url = serverURL + "/realms/" + realm + "/protocol/openid-connect/token";
        return Unirest.post(url)
                .header("Content-Type", "application/x-www-form-urlencoded")
                .field("client_id", clientID)
                .field("client_secret", clientSecret)
                .field("refresh_token", refreshToken)
                .field("grant_type", "refresh_token")
                .asJson().getBody();
    }
}

Then a service to create and log in user:

@Service
public class KeycloakAdminClientService {
    @Value("${keycloak.realm}")
    public String realm;

    private final KeycloakProvider kcProvider;


    public KeycloakAdminClientService(KeycloakProvider keycloakProvider) {
        this.kcProvider = keycloakProvider;
    }

    public Response createKeycloakUser(CreateUserRequest user) {
        UsersResource usersResource = kcProvider.getInstance().realm(realm).users();
        CredentialRepresentation credentialRepresentation = createPasswordCredentials(user.getPassword());

        UserRepresentation kcUser = new UserRepresentation();
        kcUser.setUsername(user.getEmail());
        kcUser.setCredentials(Collections.singletonList(credentialRepresentation));
        kcUser.setFirstName(user.getFirstname());
        kcUser.setLastName(user.getLastname());
        kcUser.setEmail(user.getEmail());
        kcUser.setEnabled(true);
        kcUser.setEmailVerified(false);
        return usersResource.create(kcUser);

    }

    private static CredentialRepresentation createPasswordCredentials(String password) {
        CredentialRepresentation passwordCredentials = new CredentialRepresentation();
        passwordCredentials.setTemporary(false);
        passwordCredentials.setType(CredentialRepresentation.PASSWORD);
        passwordCredentials.setValue(password);
        return passwordCredentials;
    }


}

Finally, create a controller to create and login user:

@RestController
@RequestMapping("/user")
public class UserController {
    private final KeycloakAdminClientService kcAdminClient;

    private final KeycloakProvider kcProvider;

    private static final Logger LOG = org.slf4j.LoggerFactory.getLogger(UserController.class);


    public UserController(KeycloakAdminClientService kcAdminClient, KeycloakProvider kcProvider) {
        this.kcProvider = kcProvider;
        this.kcAdminClient = kcAdminClient;
    }
	

    @PostMapping(value = "/create")
    public ResponseEntity<Response> createUser(@RequestBody CreateUserRequest user) {
        Response createdResponse = kcAdminClient.createKeycloakUser(user);
        return ResponseEntity.ok(createdResponse);

    }

    @PostMapping("/login")
    public ResponseEntity<AccessTokenResponse> login(@NotNull @RequestBody LoginRequest loginRequest) {
        Keycloak keycloak = kcProvider.newKeycloakBuilderWithPasswordCredentials(loginRequest.getUsername(), loginRequest.getPassword()).build();

        AccessTokenResponse accessTokenResponse = null;
        try {
            accessTokenResponse = keycloak.tokenManager().getAccessToken();
            return ResponseEntity.status(HttpStatus.OK).body(accessTokenResponse);
        } catch (BadRequestException ex) {
            LOG.warn("invalid account. User probably hasn't verified email.", ex);
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(accessTokenResponse);
        }

    }

}

Too much code? No worries, they are all available on Github.

Now, open postman and call these endpoints. First, create a user:

If you login with that user now, you will get an error since the user hasn’t got email verified:

Simply switch email verified on then y ou can get the tokens:

Notice that you need to put email in the place of user name because, by default, email is used as username:

Obviously, you can turn this off to log in with the username.

Conclusion

In this super long post, I created a project that help you to quickly configure keycloak with your spring boot app. You can now use this project as a base and start creating your app’s business logic.

Code repo is here: https://github.com/datmt/Keycloak-Spring-Boot-Login-Create-User

Posted on Leave a comment

Ubuntu package management cheat sheet

Most used commands (copy directly from the man page)

Most used commands:
  list - list packages based on package names
  search - search in package descriptions
  show - show package details
  install - install packages
  reinstall - reinstall packages
  remove - remove packages
  autoremove - Remove automatically all unused packages
  update - update list of available packages
  upgrade - upgrade the system by installing/upgrading packages
  full-upgrade - upgrade the system by removing/installing/upgrading packages
  edit-sources - edit the source information file
  satisfy - satisfy dependency strings

To remove a package

sudo apt remove package-name

To install a package

sudo apt install package-name

List installed packages

apt list --installed

Search an installed package

apt list --installed | grep package-name

To remove mulitple packages using regex wildcard:

sudo apt remove -s '*pattern*'

For example, you want to remove all php related packages on your system, you can do like this:

sudo apt remove -s '*php*'

You’ll see messages like these:

It is because apt tries to remove all packages which match that pattern. Since, for example, conquest-mysql not installed in my system, the remove request for that package is invalid.

However, if there are packages installed on the system, they will be removed.

For example, I tried to remove thunderbird. Since there are packages installed, they were removed:

Cleanup packages that no longer required

There are times you installed a package, some other dependencies are installed too. However, when you remove the package you installed, the dependencies may not be removed. The following command helps cleanup those redundant, left-over packages:

sudo apt autoremove

Posted on Leave a comment

Top Useful Copy-Paste Commands For Apache Solr

Create an user

To create an user in Solr, first, you need to enable authentication plugin by creating a file named security.json under /var/solr/data.

Enter into your Solr environment (docker or command line where you have Solr installed) and execute the following command:

cat << EOF > /var/solr/data/security.json
{
"authentication":{ 
   "blockUnknown": true, 
   "class":"solr.BasicAuthPlugin",
   "credentials":{"solr":"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c="}, 
   "forwardCredentials": false 
},
"authorization":{
   "class":"solr.RuleBasedAuthorizationPlugin",
   "permissions":[{"name":"security-edit",
      "role":"admin"}],
    "user-role":{"solr":"admin"} 
}
}
EOF

This command creates the security.json file at the correct location and also create an admin user with username solr and password SolrRocks.

Restart Solr (if you use docker, type docker restart your_solr_container).

Now, you have Solr authentication plugin enabled. You can use the user solr to create a new user.

curl --user solr:SolrRocks http://localhost:8983/solr/admin/authentication -H 'Content-type:application/json' -d '{"set-user": {"new_solr_user_name" : "strong_new_password" }}'

After that, you should delete the user solr by delete all records related to this user in security.json file.

Here is the security.json file after a new user is created:

{
  "authentication":{
    "blockUnknown":true,
    "class":"solr.BasicAuthPlugin",
    "credentials":{
      "solr":"IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c=",
      "new_solr_user_name":"H+x9i6Zy9U2sBQ9cDcAP/ddd IIpBHq7wyOQH2BnqSNYHeo4QL+lY4QSuaVmn5ma8lQI="},
    "forwardCredentials":false,
    "":{"v":0}},
  "authorization":{
    "class":"solr.RuleBasedAuthorizationPlugin",
    "permissions":[{
        "name":"security-edit",
        "role":"admin"}],
    "user-role":{"solr":"admin"}}
}

Now, delete solr under credential and replace solr with your new username at line 15.

The new content should look like this:

{
  "authentication":{
    "blockUnknown":true,
    "class":"solr.BasicAuthPlugin",
    "credentials":{
      "new_solr_user_name":"H+x9i6Zy9U2sBQ9cDcAP/ddd IIpBHq7wyOQH2BnqSNYHeo4QL+lY4QSuaVmn5ma8lQI="},
    "forwardCredentials":false,
    "":{"v":0}},
  "authorization":{
    "class":"solr.RuleBasedAuthorizationPlugin",
    "permissions":[{
        "name":"security-edit",
        "role":"admin"}],
    "user-role":{"new_solr_user_name":"admin"}}
}

Restart Solr and you now have a new instance with authentication plugin enabled with a secure login credentials.

Create a new collection/core

The best way to create a new core is from the command line, when you haven’t enabled the security plugin.

Creating is simple like this:

/opt/solr/bin/solr create -c core_name

Things will be a bit complicated when you have already enabled the security plugin. However, it’s not possbile.

The first thing is to create a folder with the same name as your core under /var/solr/data. For example, you want to create a core named my_new_core, folder /var/solr/data/my_new_core must be available.

Next, download this file:

and decompress into that folder above.

Now, you can create a new core with curl:

curl --user your_user:SomePW  "http://localhost:8983/solr/admin/cores?action=CREATE&name=your_new_core_name"

Import data from SQL database

A common scenario for Solr user is to index data from database. You can use Solr client to do so. However, if you don’t have custom document schema, setting up a config file and use data import function would be a quicker choice.

First, create a file name db-data-config.xml under your core’s conf folder with the following content:

<dataConfig>

<dataSource type="JdbcDataSource"
           name="sentences"
           driver="org.postgresql.Driver"
           url="jdbc:postgresql://postgres:5432/pgsentences"
           user="db_user"
           password="db_pass"/>

       <document>
        <entity name="sentence" transformer="TemplateTransformer"
            dataSource="sentences"
            query="SELECT * FROM sentences"
            deltaImportQuery="SELECT * FROM scripts WHERE id>'${dih.delta.id}"
            >

            <field name="sentence" column="sentence" />
            <field name="word_count" column="word_count" />
        </entity>

       </document>
</dataConfig>

In the example above, I configure a connection to a PostgreSQL database.

I also defined a document with columns from the tables.

Save that file and in solrconfig.xml under the same folder, refer to this configuration as below:

 <requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
    <lst name="defaults">
        <str name="config">db-data-config.xml</str>
    </lst>
</requestHandler>

If you connection to SQL databases, you may need to download your database’s driver so Solr can connect and index the data.

Also in solrconfig.xml

<lib dir="${solr.install.dir:../../../..}/dist/" regex="postgresql-.*\.jar" />

As you can see, the driver jar file is placed under /opt/solr/dist

You also need to add other jar files into solrconfig.xml

	<lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-dataimporthandler-.*\.jar" />
    <lib dir="${solr.install.dir:../../../..}/dist/" regex="mysql-connector-java-.*\.jar" />
    <lib dir="${solr.install.dir:../../../..}/dist/" regex="sqlite-jdbc-.*\.jar" />
    <lib dir="${solr.install.dir:../../../..}/dist/" regex="postgresql-.*\.jar" />
  
    <lib dir="${solr.install.dir:../../../..}/contrib/extraction/lib" regex=".*\.jar" />
    <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-cell-\d.*\.jar" />
    <lib dir="${solr.install.dir:../../../..}/contrib/clustering/lib/" regex=".*\.jar" />
    <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-clustering-\d.*\.jar" />
    <lib dir="${solr.install.dir:../../../..}/contrib/langid/lib/" regex=".*\.jar" />
    <lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-langid-\d.*\.jar" />

Up to this point, you can restart Solr and have the database indexed. However, the result may not what you expected. You also need to define the document schema in managed-schema file.

Here, you define each field of the document. Here is an example:

    <field name="id" type="string" indexed="true" stored="true" required="true" multiValued="false" />
    <field name="word_count" type="plong" indexed="true" stored="true" multiValued="false" />
    <field name="correction_script" type="string" indexed="true" stored="true" multiValued="false" />
    <field name="speech_analyzed" type="string" indexed="true" stored="true" multiValued="false" />
    <field name="file_name" type="string" indexed="true" stored="true" required="true" multiValued="false" />
    <!-- docValues are enabled by default for long type so we don't need to index the version field  -->
    <field name="_version_" type="plong" indexed="false" stored="false"/>

Make sure the field’s names here match the fields you described in db-data-config.xml

Also, if your field could be null, you must not set required attribute.

Now, restart solr and you are ready to import.

Select the core, go to dataimport and click on Execute. You should see something like this:

If the process stops instantly, there could be something wrong. You may need to check the Log area for more details:

solr logging for details of errors

If you have a lot of data (like me), it make take a while to completely index your database.

Increase max memory for Solr

Initially, Solr set 512MB for heap memory. For small data, that would be enough. However, there are times you notice that your Solr instance keeps shutting down because of lack of memory.

That’s the time you need to increase Solr’s memory.

Increasing memory for Solr is quite simple. Simply only the file /opt/solr/bin/solr in a text editor and find the block where Solr set the memories (you can search for the 512 value).

The original block looks like this:

Then edit like this:

In the screenshot, you can see that I changed the max value to 2G.

I’ve copied the text here for your convenience:

JAVA_MEM_OPTS=()
if [ -z "$SOLR_HEAP" ] && [ -n "$SOLR_JAVA_MEM" ]; then
  JAVA_MEM_OPTS=($SOLR_JAVA_MEM)
else
  SOLR_HEAP_MIN="${SOLR_HEAP:-512m}"
  SOLR_HEAP_MAX="${SOLR_HEAP:-3g}"
  JAVA_MEM_OPTS=("-Xms$SOLR_HEAP_MIN" "-Xmx$SOLR_HEAP_MAX")
fi

Save and restart solr and you should see the changes updated:

Update solr max memory settings