<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Artur.Rocks.Indeed]]></title><description><![CDATA[All-around tech blog]]></description><link>https://artur.rocks/</link><image><url>https://artur.rocks/favicon.png</url><title>Artur.Rocks.Indeed</title><link>https://artur.rocks/</link></image><generator>Ghost 4.10</generator><lastBuildDate>Tue, 01 Jul 2025 18:15:10 GMT</lastBuildDate><atom:link href="https://artur.rocks/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[I set up Authentik Outpost for K3S with Traefik and it works!]]></title><description><![CDATA[<p>TLDR; If you end up on this page, you probably realize that configuring domain-level Forward Auth from Traefik in K3S with Authentik 2024.2.2 is not easy. I show here how to do it.</p><p>Authentik is a popular identity provider which can be used in various authentication and authorisation</p>]]></description><link>https://artur.rocks/i-set-up-authentik-outpost-for-k3s-with-traefik/</link><guid isPermaLink="false">661c032bce1e4700010fe110</guid><dc:creator><![CDATA[Artur]]></dc:creator><pubDate>Sun, 14 Apr 2024 19:34:47 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1617268908644-45b1a9bd66fd?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fG91dHBvc3R8ZW58MHx8fHwxNzE1NDY1NzE1fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1617268908644-45b1a9bd66fd?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fG91dHBvc3R8ZW58MHx8fHwxNzE1NDY1NzE1fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="I set up Authentik Outpost for K3S with Traefik and it works!"><p>TLDR; If you end up on this page, you probably realize that configuring domain-level Forward Auth from Traefik in K3S with Authentik 2024.2.2 is not easy. I show here how to do it.</p><p>Authentik is a popular identity provider which can be used in various authentication and authorisation flows. You can get more information about it on <a href="https://goauthentik.io/">https://goauthentik.io/</a>. There is a big list of possible integrations with known services, such as Portainer and Jenkins.</p><p>The other feature of Authentik is more interesting. It provides a capability to protect web-services from unauthorized access, even if these services do not support it originally. This is achieved by intercepting incoming requests and testing if they contain a proper http-header. In case no such header is found, the request is redirected to a login page of Authentik.</p><p>It sounds great and doable. There are plenty of resources describing the configuration process. Unfortunately, I did not find a solution for K3S with Traefik. Getting it work cost me a lot of time and patience. The official documentation does not describe this case. So I would like to share this knowledge.</p><p>To be on the same page, here are the prerequisites:<br>1. Your instance of Authentik is deployed at <code>https://authentik.domain1</code><br>2. The service you would like to protect is deployed in a K3S and available at <code>https://service.domain2</code><br>3. A future Authentik Outpost will be accessible at <code>https://outpost.domain3</code>.<br>Please note, that <code>domain1</code>, <code>domain2</code> and <code>domain3</code> do not need to be different. They all can use 1 domain and be deployed in the same K3S cluster.</p><p>First of all, we install an Authentik Outpost using a helm chart from <a href="https://artifacthub.io/packages/helm/goauthentik/authentik-remote-cluster">https://artifacthub.io/packages/helm/goauthentik/authentik-remote-cluster</a>. The installation process ends with a kube-config. This config is reqiured to create an outpost integration in Authentik.</p><figure class="kg-card kg-image-card"><img src="https://artur.rocks/content/images/2024/04/Screenshot-2024-04-14-at-19.03.50.png" class="kg-image" alt="I set up Authentik Outpost for K3S with Traefik and it works!" loading="lazy" width="2000" height="1492" srcset="https://artur.rocks/content/images/size/w600/2024/04/Screenshot-2024-04-14-at-19.03.50.png 600w, https://artur.rocks/content/images/size/w1000/2024/04/Screenshot-2024-04-14-at-19.03.50.png 1000w, https://artur.rocks/content/images/size/w1600/2024/04/Screenshot-2024-04-14-at-19.03.50.png 1600w, https://artur.rocks/content/images/2024/04/Screenshot-2024-04-14-at-19.03.50.png 2078w" sizes="(min-width: 720px) 720px"></figure><p>Later, you can use the integration to create an outpost for an app of your choice. The created outpost will automatically trigger a new <code>ghcr.io/goauthentik/proxy</code> deployment in the cluster which will connect to Authentik.<strong><em> </em></strong></p><p><strong><em>Here is a catch! </em></strong>The outpost needs to be exposed at <code>https://outpost.domain3</code>. There is a ping endpoint which must be testable via:<br><code>curl -i https://outpost.domain3/outpost.goauthentik.io/ping</code>. A correct response contains <code>HTTP/2 204</code>. One way to expose the outpost is using Ingress:</p><pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: outpost-ingress
spec:
  ingressClassName: traefik
  rules:
  - host: outpost.domain3
    http:
      paths:
      - backend:
          service:
            name: ak-outpost
            port:
              number: 9000
        path: /
        pathType: Prefix</code></pre><p>Finally, the target web-service needs to be re-configured to use the following Traefik Middleware with the exposed outpost:</p><pre><code>apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: authentik
spec:
  forwardAuth:
    address: https://outpost.domain3/outpost.goauthentik.io/auth/traefik
    trustForwardHeader: true
    authResponseHeaders:
      - X-authentik-username
      - X-authentik-groups
      - X-authentik-email
      - X-authentik-name
      - X-authentik-uid
      - X-authentik-jwt
      - X-authentik-meta-jwks
      - X-authentik-meta-outpost
      - X-authentik-meta-provider
      - X-authentik-meta-app
      - X-authentik-meta-version
</code></pre><p>The target web-service can be exposed via: </p><pre><code>---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: whoami-ingress
  annotations:
    traefik.ingress.kubernetes.io/router.middlewares: default-authentik@kubernetescrd
spec:
  ingressClassName: traefik
  rules:
    - host: https://service.domain2
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: whoami
                port:
                  number: 80</code></pre><p>This configuration works for domain-level forward auth, where an auth cookie is set for the whole <code>domain2</code> and all services within the domain, e.g. <code>service2.domain2</code>.</p><p>Single app forward auth is also possible, but needs changes to Ingress. Let me know if you are interested.</p>]]></content:encoded></item><item><title><![CDATA[JPA Specifications with Group By Aggregation]]></title><description><![CDATA[In this article I demonstrate how to enable JPA to produce SQL queries dynamically and execute them.]]></description><link>https://artur.rocks/jpa-specifications-with-group-by-aggregation/</link><guid isPermaLink="false">627a80b1cba7e400015920bd</guid><category><![CDATA[java]]></category><dc:creator><![CDATA[Artur]]></dc:creator><pubDate>Tue, 10 May 2022 19:27:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1652138896996-0c9ff7f19dac?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8YWxsfDI1fHx8fHx8Mnx8MTY1MjIxMDc2Ng&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1652138896996-0c9ff7f19dac?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8YWxsfDI1fHx8fHx8Mnx8MTY1MjIxMDc2Ng&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="JPA Specifications with Group By Aggregation"><p>In this article I demonstrate how to enable JPA to produce SQL queries dynamically and execute them, for example:</p><!--kg-card-begin: markdown--><pre><code>select book_type, count(book.id) 
from book
where book.publish_date&lt;=PARSEDATETIME(&apos;01-01-1900&apos;,&apos;dd-MM-yyy&apos;)
group by book.book_type
</code></pre>
<!--kg-card-end: markdown--><p>It is easy to use named queries and predefined method from JPA to query a database. However, this may not be enough. Some situations require generation of SQL queries during runtime, e.g. filtering or aggregation based on user-specified fields.</p><p>We start with a basic entity:</p><!--kg-card-begin: markdown--><pre><code>@Entity
@Table(name = &quot;Book&quot;)
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String author;

    @Column
    private String title;

    @Column
    private Date publishDate;
    
    @Enumerated(EnumType.STRING)
    private BookType bookType;
}
</code></pre>
<!--kg-card-end: markdown--><p>The following repository definition allows us to access and to query the db: </p><!--kg-card-begin: markdown--><pre><code>@Repository
public interface BookRepo extends JpaRepository&lt;Book, Long&gt;, JpaSpecificationExecutor&lt;Book&gt;, GroupByRepository{
}
</code></pre>
<!--kg-card-end: markdown--><p>Extension of JpaSpecificationExecutor interface adds a support of <a href="https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/">JPA Specifications</a>. This allows us to dynamically generate where-statements in the SQL query.</p><p>The interface GroupByRepository adds the aggregation functionality. Here is the definition: </p><!--kg-card-begin: markdown--><pre><code>public interface GroupByRepository {
    Map&lt;Object, Long&gt; whereGroupBy(SingularAttribute singularAttribute, Specification where);
}
</code></pre>
<!--kg-card-end: markdown--><p>The only method of the interface takes an attribute of the entity as a group-by argument, and the mentioned JPA specification. Here is an implementation of the interface, inspired by a post from <a href="https://stackoverflow.com/a/54303286">stackoverflow</a>:</p><!--kg-card-begin: markdown--><pre><code>public class GroupByRepositoryImpl implements GroupByRepository {
    @Autowired
    private EntityManager entityManager;
    @Override
    public Map&lt;Object, Long&gt; whereGroupBy(SingularAttribute singularAttribute, Specification where) {
        final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        final CriteriaQuery&lt;Tuple&gt; query = criteriaBuilder.createQuery(Tuple.class);
        final Root&lt;Book&gt; root = query.from(Book.class);
        final Path&lt;String&gt; expression = root.get(singularAttribute);
        query.multiselect(expression, criteriaBuilder.count(root));
        query.select(criteriaBuilder.tuple(expression, criteriaBuilder.count(root)));
        query.where(where.toPredicate(root, query, criteriaBuilder));
        query.groupBy(expression);
        final List&lt;Tuple&gt; resultList = entityManager.createQuery(query).getResultList();
        return resultList.stream()
                .collect(toMap(
                        t -&gt; t.get(0, singularAttribute.getJavaType()),
                        t -&gt; t.get(1, Long.class))
                );
    }
}
</code></pre>
<!--kg-card-end: markdown--><p>The following JUnit test contains an example of how to use the method:</p><!--kg-card-begin: markdown--><pre><code>@RunWith(SpringRunner.class)
@SpringBootTest
public class GroupByRepositoryTest {
    @Autowired
    BookRepo bookRepo;

    @Test
    public void groupByTest() {
        final Date Century20th = new Date(-2199999999999l);
        System.out.println(Century20th.toString());
        Specification&lt;Object&gt; where = Specification.where(
                (root, query, cb) -&gt; cb.lessThanOrEqualTo(root.&lt;Date&gt;get(&quot;publishDate&quot;), Century20th)
        );
        Map&lt;Object, Long&gt; result = bookRepo.whereGroupBy(Book_.bookType, where);
        System.out.println(result);
        Assert.assertEquals(2, result.entrySet().size());
    }
}
</code></pre>
<!--kg-card-end: markdown--><p>I showed you how you can use JPA Specification API to generate type-safe SQL queries dynamically instead of using query strings. </p><p>The sources are available <a href="https://drive.google.com/file/d/1g_C4GGkq9ux-akQkXwk0vRztHo6UVS-m/view?usp=sharing">here</a>. Hopefully, you find this post useful.</p>]]></content:encoded></item><item><title><![CDATA[On Integration Testing using JUnit 5 in Spring Boot 2.4.4 with Spring Batch]]></title><description><![CDATA[I show how to configure Spring Boot 2.4.4 to run Intergration Tests in Spring Batch]]></description><link>https://artur.rocks/on-integration-testing-using-junit-5/</link><guid isPermaLink="false">606b3a23ad8269000175251a</guid><category><![CDATA[java]]></category><dc:creator><![CDATA[Artur]]></dc:creator><pubDate>Tue, 10 May 2022 15:01:54 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1518133910546-b6c2fb7d79e3?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDZ8fHRlc3R8ZW58MHx8fHwxNjE3NjQzMDQx&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1518133910546-b6c2fb7d79e3?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDZ8fHRlc3R8ZW58MHx8fHwxNjE3NjQzMDQx&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="On Integration Testing using JUnit 5 in Spring Boot 2.4.4 with Spring Batch"><p>Here is a pom.xml:</p><!--kg-card-begin: markdown--><pre><code>
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;
         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;

    &lt;parent&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
        &lt;version&gt;2.4.4&lt;/version&gt;
        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;
    &lt;/parent&gt;

    &lt;groupId&gt;org.example&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot&lt;/artifactId&gt;
    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;

    &lt;properties&gt;
        &lt;maven.compiler.source&gt;8&lt;/maven.compiler.source&gt;
        &lt;maven.compiler.target&gt;8&lt;/maven.compiler.target&gt;
    &lt;/properties&gt;

    &lt;dependencies&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
            &lt;scope&gt;test&lt;/scope&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.junit.jupiter&lt;/groupId&gt;
            &lt;artifactId&gt;junit-jupiter&lt;/artifactId&gt;
            &lt;scope&gt;test&lt;/scope&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-batch&lt;/artifactId&gt;
        &lt;/dependency&gt;

        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.batch&lt;/groupId&gt;
            &lt;artifactId&gt;spring-batch-test&lt;/artifactId&gt;
            &lt;scope&gt;test&lt;/scope&gt;
        &lt;/dependency&gt;


        &lt;dependency&gt;
            &lt;groupId&gt;com.h2database&lt;/groupId&gt;
            &lt;artifactId&gt;h2&lt;/artifactId&gt;
            &lt;scope&gt;runtime&lt;/scope&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-batch&lt;/artifactId&gt;
        &lt;/dependency&gt;

    &lt;/dependencies&gt;

    &lt;build&gt;
        &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
            &lt;/plugin&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
                &lt;artifactId&gt;maven-surefire-plugin&lt;/artifactId&gt;
                &lt;configuration&gt;
                    &lt;excludes&gt;
                        &lt;exclude&gt;**/*IT.java&lt;/exclude&gt;
                    &lt;/excludes&gt;
                &lt;/configuration&gt;
                &lt;executions&gt;
                    &lt;execution&gt;
                        &lt;id&gt;integration-test&lt;/id&gt;
                        &lt;goals&gt;
                            &lt;goal&gt;test&lt;/goal&gt;
                        &lt;/goals&gt;
                        &lt;phase&gt;integration-test&lt;/phase&gt;
                        &lt;configuration&gt;
                            &lt;excludes&gt;
                                &lt;exclude&gt;**/*Test.java&lt;/exclude&gt;
                            &lt;/excludes&gt;
                            &lt;includes&gt;
                                &lt;include&gt;**/*IT.java&lt;/include&gt;
                            &lt;/includes&gt;
                        &lt;/configuration&gt;
                    &lt;/execution&gt;
                &lt;/executions&gt;
            &lt;/plugin&gt;
        &lt;/plugins&gt;
    &lt;/build&gt;

&lt;/project&gt;


</code></pre>
<!--kg-card-end: markdown--><p>As you can see maven-surefire-plugin allows us to execute integration tests separately from Unit tests. This enables faster development cycles without a need to wait until integration tests are finished.</p><p></p><p>Here is an example of a unit test:</p><!--kg-card-begin: markdown--><pre><code>
@SpringBootTest
@AutoConfigureMockMvc
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void index() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get(&quot;/&quot;)
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo(&quot;greetings&quot;)));



    }
}
</code></pre>
<!--kg-card-end: markdown--><p>An example of an integration test follows:</p><!--kg-card-begin: markdown--><pre><code>package com.example.springboot;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;

import java.net.URL;
import java.util.Collection;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

@ActiveProfiles(&quot;it&quot;)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloControllerIT {
    @LocalServerPort
    private int port;

    private URL base;

    @Autowired
    private TestRestTemplate template;

    @BeforeEach
    public void setUp() throws Exception {
        this.base = new URL(&quot;http://localhost:&quot; + port + &quot;/&quot;);

    }

    @Test
    public void getHello() throws Exception {
        ResponseEntity&lt;String&gt; response =
            template.getForEntity(base.toString(), String.class);
        assertThat(response.getBody()).isEqualTo(&quot;greetings&quot;);
    }

    @Test
    public void getStudents() throws Exception {
        ResponseEntity&lt;Collection&gt; response = 
            template.getForEntity(base.toString() + &quot;students&quot;, 
                                  Collection.class);
        assertThat(response.getBody().size()).isEqualTo(3);
    }
}
</code></pre>
<!--kg-card-end: markdown--><p>Finally here is an integration test of a Spring Batch pipeline:</p><!--kg-card-begin: markdown--><pre><code>package com.example.springboot;

import com.example.springboot.persistence.StudentRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.batch.test.JobRepositoryTestUtils;
import org.springframework.batch.test.context.SpringBatchTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;

import javax.sql.DataSource;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;


@SpringBatchTest
@SpringBootTest
@DirtiesContext
@ActiveProfiles(&quot;it&quot;)
public class SpringBatchIT {

    @Autowired
    DataSource dataSource;

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Autowired
    private JobRepositoryTestUtils jobRepositoryTestUtils;

    @Autowired
    private StudentRepository studentRepository;

    @AfterEach
    public void cleanUp() {
        jobRepositoryTestUtils.removeJobExecutions();
    }

    @Test
    public void batchTest() throws Exception {

        long countBefore = studentRepository.count();
        System.out.println(String.format(&quot;There are %d objects in DB 
                                  before running the test&quot;, countBefore));
        assertThat(countBefore, is(3L));


        JobExecution jobExecution = jobLauncherTestUtils.launchJob();
        
        
        JobInstance jobInstance = jobExecution.getJobInstance();
        ExitStatus jobExitStatus = jobExecution.getExitStatus();
        assertThat(jobInstance.getJobName(), is(&quot;firstJob&quot;));
        assertThat(jobExitStatus.getExitCode(), is(&quot;COMPLETED&quot;));


        long countAfter = studentRepository.count();
        System.out.println(String.format(&quot;There are %d objects in DB after
                                          running the test&quot;, countAfter));
        assertThat(countAfter, is(2L));

    }
}
</code></pre>
<!--kg-card-end: markdown--><p>You can also notice that the integration test classes are annotated using @ActiveProfiles. This helps to differentiate between unit and integration tests. To make use of them (and save time on package phase of maven build) I introduced a config file:</p><!--kg-card-begin: markdown--><pre><code>spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName= org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

spring.batch.job.enabled=false
</code></pre>
<!--kg-card-end: markdown--><p>The last parameter in the config file disables autostart of the Spring Batch pipeline.</p>]]></content:encoded></item><item><title><![CDATA[Make QNAP NAS great and secure again]]></title><description><![CDATA[I show how to add 2-step auth when connecting over SSH to NAS, in particular NAS from QNAP. ]]></description><link>https://artur.rocks/make-qnap-nas-great-and-secure-again/</link><guid isPermaLink="false">62504a0c379e0a000174637b</guid><category><![CDATA[nas]]></category><category><![CDATA[ssh]]></category><category><![CDATA[2-step auth]]></category><dc:creator><![CDATA[Artur]]></dc:creator><pubDate>Sat, 16 Apr 2022 12:09:56 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1604398525509-ce4af98fdb23?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDE0fHxzZWN1cmV8ZW58MHx8fHwxNjUwMTEwOTY0&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1604398525509-ce4af98fdb23?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDE0fHxzZWN1cmV8ZW58MHx8fHwxNjUwMTEwOTY0&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="Make QNAP NAS great and secure again"><p>Hi folks. This post is dedicated to the owners of <a href="https://en.wikipedia.org/wiki/Network-attached_storage">NAS</a>.</p><p>NAS are not particular safe, as they become targets to hacker attacks due to vulnerabilities due to &quot;vendors don&apos;t give a sh*t&quot;. A common practice here is to block any access to NAS from the internet, which reduces their usefulness.</p><p>Here I show you one way to keep NAS secure while allowing access to it from the internet. I use a NAS from QNAP, but the approach is valid for other brands as well. <strong>Disclaimer</strong>: I am not responsible for your actions/losses and make sure you understand what you do. </p><p>The main ingredient of my solution is <a href="https://en.wikipedia.org/wiki/Secure_Shell">SSH</a>. It allows remote connections to a server and offers configurations to minimize risks of unauthorized access. One such configuration is adding a 2-step authentication, which requires a temporal verification code. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://artur.rocks/content/images/2022/04/ssh_auth_login_blurred.png" class="kg-image" alt="Make QNAP NAS great and secure again" loading="lazy"><figcaption>The login procedure in SSH using 2-step authentication.&#xA0;</figcaption></figure><p>Here are the steps:</p><ol><li><strong>Enable SSH in NAS.</strong></li></ol><p>This is the easiest step, can be done mostly from the web-ui of the NAS.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://artur.rocks/content/images/2022/04/step1.enable_ssh.png" class="kg-image" alt="Make QNAP NAS great and secure again" loading="lazy"><figcaption>This way you turn on SSH on NAS from QNAP.</figcaption></figure><p><strong>2. Install Entware.</strong></p><p>Commonly, NAS run on a linux-based OS. These OSs are very limited in terms of available software. Entware solves this problem by allowing to install a great deal of linux-packages; yes, it is a package manager. The official website is <a href="https://entware.net/">https://entware.net/</a>. </p><p>Before installing Entware on QNAP NAS, you have to add a 3-rd party repo <a href="https://www.qnapclub.eu/en/howto/1">https://www.qnapclub.eu/en/howto/1</a>. &#xA0;</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://artur.rocks/content/images/2022/04/step2.add_repo.png" class="kg-image" alt="Make QNAP NAS great and secure again" loading="lazy"><figcaption>Adding a 3-rd party software repository on QNAP NAS</figcaption></figure><p>Afterwards, Entware can be found using the search box.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://artur.rocks/content/images/2022/04/step2.install_entware.png" class="kg-image" alt="Make QNAP NAS great and secure again" loading="lazy"><figcaption>Installing Entware-std 1.03 on QNAP NAS</figcaption></figure><p><strong>3. Enable 2-step authentication.</strong></p><p>Usually, this is achieved by adding google-authenticator-libpam and tuning the SSH configuration. Here is <a href="https://ubuntu.com/tutorials/configure-ssh-2fa">a helpful tutorial</a>.</p><p>Some NAS vendors, e.g. Asustor, already pre-install all required packages. The only task is to enable the method by setting &quot;UsePAM yes&quot; in sshd_config.</p><p>Other vendors, such as QNAP, make our lives extremely difficult and such easy modifications are not feasible. However, here I show the way to achieve our goal and enable the 2-step authentication on QNAP NAS TS-253D with firmware v. 5.0.0. The following subsections are about it.</p><p><strong>3.1 Installing a separate SSH on QNAP NAS.</strong></p><p>The big problem of QNAP NAS firmware is the tools and their configs are hard-wired in the firmware. Modifications of any parameters will not survive a system restart, as the defaults are overwritten. A solution here is to store all necessary packages away from firmware-important locations and wire the SSH user to use those packages only.</p><p>This means, that we have install a separate SSH-server, which will create a new SSH session for the user. This new SSH-server can and must be configured to enable the 2-step authentication. Here is <a href="https://wiki.qnap.com/wiki/Replace_ssh_with_Qnapware_OpenSSH">an outdated guide for the firmware v. 4.2.0.</a></p><p>Let&apos;s start. Connect to the NAS using the default SSH and install the packages:</p><!--kg-card-begin: markdown--><blockquote>
<p>opkg install openssh-server-pam google-authenticator-libpam</p>
</blockquote>
<!--kg-card-end: markdown--><p>After a successful execution of the command, the new binaries will be placed in /opt/bin and /opt/sbin. The contents of /opt survive during NAS restarts.</p><p>Add the bin folders to $PATH:</p><!--kg-card-begin: markdown--><blockquote>
<p>export PATH=/opt/bin:/opt/sbin:$PATH</p>
</blockquote>
<!--kg-card-end: markdown--><p><a href="https://wiki.qnap.com/wiki/Running_Your_Own_Application_at_Startup">This tutorial</a> shows how to add commands to autorun.sh, which will be automatically started on NAS restarts.</p><p><strong>3.2 Configuring the SSH server.</strong></p><p>Execute the commands:</p><!--kg-card-begin: markdown--><blockquote>
<p>ssh-keygen -f /opt/etc/ssh/ssh_host_rsa_key -t rsa<br>
ssh-keygen -f /opt/etc/ssh/ssh_host_dsa_key -t dsa<br>
ssh-keygen -f /opt/etc/ssh/ssh_host_ecdsa_key -t ecdsa<br>
ssh-keygen -f /opt/etc/ssh/ssh_host_ed25519_key -t ed25519<br>
useradd --system --no-create-home sshd<br>
google-authenticator</p>
</blockquote>
<!--kg-card-end: markdown--><p>The last command will start a wizard to generate required tokens with QR-code needed for your OTP-app. </p><p>Add the following lines to /opt/etc/pam.d/sshd:</p><!--kg-card-begin: markdown--><blockquote>
<p>#Google Authenticator 2-step auth.<br>
auth       required    pam_google_authenticator.so nullok</p>
</blockquote>
<!--kg-card-end: markdown--><p>Update /opt/etc/ssh/sshd_config with:</p><!--kg-card-begin: markdown--><blockquote>
<p>Port 322<br>
UsePAM yes<br>
ChallengeResponseAuthentication yes</p>
</blockquote>
<!--kg-card-end: markdown--><p>Lastly, update the autorun.sh with (this enables autostart of the new SSH server):</p><!--kg-card-begin: markdown--><blockquote>
<p>/opt/etc/init.d/S40sshd start</p>
</blockquote>
<!--kg-card-end: markdown--><p>In case the server is not started yet, you can manually execute the command.</p><p><strong>4. Expose the endpoint.</strong></p><p>Once the SSH-server is started, you can try to connect to it using the internal IP of the NAS, e.g.:</p><!--kg-card-begin: markdown--><blockquote>
<p>ssh -p 322 {user}@{NAS_IP}</p>
</blockquote>
<!--kg-card-end: markdown--><p>If you want to access the NAS using SSH from the internet, you need to set up port-forwarding on your router (and, likely, to talk to you provider to turn on the feature). Make sure that you port-forward the new SSH server with port 322 (with enabled 2-step auth). Otherwise, with the default SSH server exposed to the internet, you put your NAS and local network in a great danger! </p><p><strong>Final thoughts</strong></p><p>I showed you here how you can make your NAS more useful by enabling a secure access to it over the internet. SSH is the most important service and the key to further possibilities, such as:</p><ul><li>A remote network folder (over SFTP) to store and access your files.</li><li>a remote desktop environment (over ssh -X).</li><li>a private streaming service, where your own music/movies/shows can be accessed via Kodi/Jellyfin.</li></ul><p>Please let me know in the comments, what you want to learn next.</p>]]></content:encoded></item><item><title><![CDATA[Building a self-hosted blog from scratch.]]></title><description><![CDATA[There are many websites, e.g. WordPress, LifeJournal etc., that allow you create a space for your blog. Such websites are usually very easy to use and do not require any maintenance effort.

But this is not our way. Here we want to learn and try out cool technology.  :)]]></description><link>https://artur.rocks/from-zero-to-fully-fledged-blog-in-2020/</link><guid isPermaLink="false">5ea0949096c2d2000171d883</guid><category><![CDATA[docker]]></category><category><![CDATA[traefik]]></category><category><![CDATA[ghost]]></category><category><![CDATA[ubuntu]]></category><dc:creator><![CDATA[Artur]]></dc:creator><pubDate>Tue, 19 May 2020 20:32:15 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1542899568-4eea00a3c2f9?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1542899568-4eea00a3c2f9?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Building a self-hosted blog from scratch."><p>Here I show you the easiest way to start your own blog. Make sure you have a domain and a virtual server with docker behind. </p><p>We will deploy all needed software as <a href="https://docs.docker.com/get-started/"><strong>Docker containers</strong></a>. Here is a code snippet that contains a specification for docker-compose:</p><!--kg-card-begin: markdown--><pre><code>version: &apos;3&apos;

services:

  traefik:
    image: traefik:v2.2
    container_name: traefik
    networks:
      - web
    command:
      
      - --log.level=DEBUG
      - --global.sendanonymoususage=false
      - --api.insecure=true
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443

      #Enable https
      - --certificatesresolvers.mychallenge.acme.httpchallenge=true
      - --certificatesresolvers.mychallenge.acme.httpchallenge.entrypoint=web
      - --certificatesresolvers.mychallenge.acme.email=your@email
      - --certificatesresolvers.mychallenge.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.mychallenge.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory

    ports:
      - &quot;80:80&quot;
      - &quot;443:443&quot;
    volumes:
      - ./shared/letsencrypt:/letsencrypt
      - /var/run/docker.sock:/var/run/docker.sock:ro
    labels:

      # middleware redirect
      - traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)
      - traefik.http.routers.http-catchall.entrypoints=web
      - traefik.http.routers.http-catchall.middlewares=redirect-to-https

      - traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
      
      - traefik.enable=true
      - traefik.http.routers.traefik.rule=Host(`traefik.localhost`)
      - traefik.http.routers.traefik.entrypoints=websecure
      - traefik.http.services.traefik.loadbalancer.server.port=8080
      - traefik.http.routers.traefik.tls.certresolver=mychallenge
    restart: unless-stopped


  ghost:
    image: ghost:3.11
    container_name: ghost
    networks:
      - web
    environment:
      - url=https://localhost
    labels:
      - traefik.enable=true
      - traefik.http.routers.ghost.rule=Host(`localhost`)
      - traefik.http.routers.ghost.entrypoints=websecure
      - traefik.http.services.ghost.loadbalancer.server.port=2368
      - traefik.http.routers.ghost.tls.certresolver=mychallenge
    volumes:
      - ./shared/ghost:/var/lib/ghost/content


networks:
  web:
</code></pre>
<!--kg-card-end: markdown--><p>This specification is ready to run on a local Docker instance. Just paste it in a <code>docker-compose.yaml</code> and execute <code>docker compose up</code>. </p><p>This specification deploys 2 containers. The first container runs <strong><a href="https://docs.traefik.io/user-guides/docker-compose/basic-example/">Traefik</a></strong>, a powerful reverse proxy, which routes traffic to a target service. You can access the Traefik dashboard at <code>https://traefik.localhost</code>. <strong><a href="https://ghost.org/docs/concepts/introduction/">Ghost</a></strong>, a blog platform, is served by the second container. It is accessible at <code>https://localhost</code>. </p><p>As you might notice, the URLs contain <code>https</code>. When you follow the links, you browser should complain about invalid certificates and will recommend not to proceed to the websites. (Although it is completely safe to proceed in this example). Later we will learn how to generate valid certificates for your domain.</p><p>Traefik helps us to redirect our requests to the blog. You might type <code>http://localhost</code> as well and you would end-up at <code>https</code>. Besides redirection, Traefik takes care of certificate generation. Additional information on enabling HTTPS can be found here: <a href="https://docs.traefik.io/https/overview/">https://docs.traefik.io/https/overview/</a>. </p><p>The next item on the agenda is to schedule back-ups. The idea is to be able to archive the content of the blog and take the archive out of the system. The easiest way is to use existing popular version control systems such as Gitlab and Github. </p><p>An example that depicts all the discussed concepts are packed in a template, that you can <a href="https://drive.google.com/uc?id=1bAuMFh7JF8pxEC5R5Sk5HrZboq4p-q-h&amp;export=download">download here</a> (2 KB). The template has to be adapted to your infrastructure before use. </p><p>In this post, I showed you how to set up your own blog using available open-source tools. The setup requires a domain, a host and a git server for backups. The blog is one use-case of the Docker containerisation technology. In the upcoming posts, I will show how else you can use Docker to address some common needs.</p>]]></content:encoded></item><item><title><![CDATA[Hello World!]]></title><description><![CDATA[This is my very first post on this beautiful platform. ]]></description><link>https://artur.rocks/hello-world/</link><guid isPermaLink="false">5ea08e2d96c2d2000171d851</guid><dc:creator><![CDATA[Artur]]></dc:creator><pubDate>Wed, 22 Apr 2020 18:41:50 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1462795532207-33cabf8c8175?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1462795532207-33cabf8c8175?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Hello World!"><p>This is my very first post on this beautiful platform. </p><p>I hope this blog will bring a lot of joy by expressing myself to me and useful thoughts, ideas and inspiration to my fellow readers. </p><p>I would like to keep the blog as open as possible, thus I will keep the comments section open for the public.</p>]]></content:encoded></item></channel></rss>