Skip to content
Documentation

Administration guide

Hints, tips and best practices for the administration of ProjectForge

Written by
Table of Contents

1. Introduction

This document describes the administration of ProjectForge®: configuration, security, user directories, backups and maintenance. For installing ProjectForge (Docker or JAR, setup wizard, reverse proxy) please refer to Installation first.

All settings mentioned here go into projectforge.properties in ProjectForge’s home directory (written by the setup wizard), unless stated otherwise. The defaults with comments are listed in application.properties. Restart ProjectForge after changing this file.

2. ProjectForge Setup

2.1. Database

The setup wizard writes the database settings, so you usually don’t have to touch them. ProjectForge uses the connection pool of Spring Boot (HikariCP); the spring.datasource.hikari.* defaults are listed in application.properties.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Embedded HSQLDB (default, for tests and small installations):
spring.datasource.url=jdbc:hsqldb:file:${projectforge.base.dir}/database/projectforge;shutdown=true
spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver
spring.datasource.username=sa
spring.datasource.password=

# PostgreSQL (recommended for productive use):
#spring.datasource.url=jdbc:postgresql://localhost:5432/projectforge
#spring.datasource.driver-class-name=org.postgresql.Driver
#spring.datasource.username=projectforge
#spring.datasource.password=<your-password>

The schema is created and migrated automatically by Flyway on every start.

2.2. Initialization, first login and set-up dialogue

After the first start, ProjectForge® shows its setup page. Choose the target (empty database for a productive system, or a test system with example data) and set the administrator’s user name and password. All other settings such as time zone and e-mail addresses can be changed later under Administration → Configuration. See Installation – The setup page for details.

2.3. SSL / Https

It’s recommended to run ProjectForge on a localhost http port and to handle https with a reverse proxy such as NGINX, see Installation – Secure http connection.

For testing purposes you can use the following configuration:

2.3.1. Self-signed certificate

Generate a self-signed certificate: keytool -genkeypair -alias selfsigned -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore keystore.p12 -validity 365

2.3.2. Configure the file ~/ProjectForge/projectforge.properties

1
2
3
4
5
6
# Bind to all interfaces, or localhost
server.address=0.0.0.0
server.port=8443
server.ssl.key-store=file:/home/projectforge/keystore.p12
server.ssl.key-store-password=<password of your certificate>
server.ssl.key-store-type=PKCS12

2.3.3. Trust the self-signed certificate

  1. Extract: keytool -exportcert -alias selfsigned -keystore keystore.p12 -storetype PKCS12 -file selfsigned.crt -rfc

  2. Import it into the certificate store of your system or browser and trust it.

2.3.4. Behind a reverse proxy: secure cookie, HSTS and CSP

If the reverse proxy terminates https, ProjectForge itself only sees http requests. Then the session cookie lacks the Secure flag and no Strict-Transport-Security header (HSTS) is sent. Tell ProjectForge about the original protocol in ~/ProjectForge/projectforge.properties:

# The proxy must send X-Forwarded-Proto (see dev-docs/misc/nginx_sites-available_projectforge):
server.forward-headers-strategy=native
# Or, if every access is via https anyway:
#server.servlet.session.cookie.secure=true

ProjectForge sends its security headers itself (Content Security Policy, X-Frame-Options, Referrer-Policy, Permissions-Policy etc.). Don’t add them again in the proxy: a header sent twice with different values is ignored by the browser. The Content Security Policy can be replaced by an own value of projectforge.security.csp-header-value (an empty value sends none, which isn’t recommended).

2.3.5. Behind a reverse proxy: live updates of the chat

The chat pushes new messages to the open conversations via server-sent events (/rs/chat/conversation/{id}/events), a long-lived response with a heartbeat every 25 seconds. The proxy must neither buffer it nor close it before the next heartbeat. ProjectForge sends X-Accel-Buffering: no, which nginx honours, so nginx only needs a read timeout above 25 seconds (the example configuration dev-docs/misc/nginx_sites-available_projectforge has 600 seconds). Other proxies need the buffering switched off for this path. Without it, the chat still works: it falls back to asking for new messages every few seconds.

For the first link of a chat message the server fetches the linked page (http and https on ports 80 and 443) and shows its title, description and image as a preview. It connects to public addresses only, never to the internal network (private, loopback and link-local addresses are refused, also after a redirect), and needs outgoing access to the internet; without it there are simply no previews. To fetch nothing from other sites at all:

projectforge.chat.linkPreview.enabled=false

2.4. PostgreSQL

2.4.1. Setting up the database

PostgreSQL 13 or newer is required: a database migration (Flyway V8.0.24) runs CREATE EXTENSION IF NOT EXISTS pg_trgm for the trigram indexes of the history search. pg_trgm is a trusted extension since PostgreSQL 13, so it’s sufficient that the database user of ProjectForge has the CREATE privilege on the database (e.g. as its owner, see below). Otherwise a database administrator has to create the extension once beforehand: psql -U postgres -d projectforge -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;".

The files of the file store are stored in an own schema pf_files, which is created by Flyway as well (so the database user also needs the right to create schemas, which the owner of the database has), see Backup.

  1. Create the user (e.g. as admin user): createuser -U postgres -P projectforge

  2. Create the database: createdb -U postgres -E UTF-8 -O projectforge projectforge

  3. You can drop an existing user and database with: dropdb projectforge and dropuser projectforge

  4. To re-use an existing database, restore a dump, see Restoring a backup.

Recommended settings in postgresql.conf (max_connections must be larger than ProjectForge’s connection pool, spring.datasource.hikari.maximum-pool-size, default 100):

max_connections = 120
statement_timeout =  300000 # 300s
lock_timeout = 300000       # 300s

3. Configuration

3.1. Base directory (application’s home)

ProjectForge® needs a home directory, by default ${user.home}/ProjectForge (/ProjectForge inside Docker). It’s chosen by the setup wizard, see Installation for how ProjectForge finds it. The following files and directories are created if they don’t exist:

projectforge.properties

Configuration file, written by the setup wizard. A former config.xml is taken over into the configuration parameters on the first start and renamed to config.xml.migrated (may be removed afterwards).

database

The embedded database (HSQLDB), if used.

hibernateSearch

The search indices (Lucene). If corrupted, stop ProjectForge, delete the content of this directory and rebuild the indices via Administration → System → Reindex.

logs

The log files, e.g. ProjectForge.log.

resources

Your own resources, overriding the built-in ones: images (logos), fonts (for the PDF renderer), mail (e-mail templates), translations.

backup

Default directory for backups.

datatransfer

The files of the data transfer areas (plugin Data transfer).

jcr

The former document repository (JCR), not used anymore since 9.2. May be deleted, see Updating from an installation with JCR.

work

Working directory, e.g. for states of synchronizations.

tmp

Temporary files, e.g. debug logs.

The directories work, tmp and backup may be placed elsewhere by the properties projectforge.dir.work, projectforge.dir.tmp and projectforge.dir.backup (absolute or relative to the home directory).

3.2. Configuration file application.properties

ProjectForge® is a Spring Boot application, so you may use any of the common application properties in projectforge.properties. All ProjectForge-specific options with their defaults are listed in application.properties.

3.2.1. Properties editor

Administrators edit projectforge.properties on the page Administration → System (menu Configuration → Configuration file). The editor lists all parameters of application.properties, grouped by topic, with their descriptions and defaults. Parameters of your file that ProjectForge doesn’t know are marked: those in ProjectForge’s namespace (projectforge., pf.) are no longer used (or misspelled) and are listed in the group No longer used, where they can be removed at once; others (e.g. settings of Spring Boot) in the group Other. Passwords, keys and tokens are never shown: a secret can only be set to a new value.

  • Before every save, a copy of the file with a timestamp is placed next to it (projectforge.properties.<yyyy-MM-dd_HH-mm-ss>.bak). The file is written sorted by groups; your own comments stay above the parameter they were written above.

  • The descriptions contain links to the matching sections of this guide.

  • Changes take effect only after a restart of ProjectForge.

  • A value the running system uses differently (a restart is pending, or the value is set by an environment variable or the command line) is marked.

3.3. Switching off functions

Functions of ProjectForge can be switched off in projectforge.properties (a restart is needed). A switched off function has no menu entry, and its pages and REST services answer with 404, for everybody.

# Allowlist: all (default) or a comma separated list.
projectforge.features.enabled=all
# Denylist, wins over the allowlist.
projectforge.features.disabled=TIMESHEET_LIST,HR

There is one function per menu entry. Its id is the id of the menu entry (ADDRESS_LIST, TIMESHEET_LIST, FEEDBACK, MY_ACCOUNT, …​, see MenuItemDefId), of a plugin’s menu entry (toDo, DataTransferPersonalBox) or of a plugin (datatransfer, marketing). A main menu group (COMMON, FIBU, HR, MISC, …​) or a plugin stands for all of its entries. Plugins that aren’t activated are switched off as well, including their REST services. Unknown ids are logged as a warning on start-up.

This is no permission system, the access checks of the functions remain as they are. Some services stay reachable even with their function switched off, because other pages need them: the lookups of the input fields (e.g. of users or tasks), the subscription of calendars, and the core of the application (login, menu, settings, notifications, attachments of the remaining functions). The interfaces outside the pages and REST services aren’t switched off either: CardDAV (/carddav), the export of calendars (/export/ProjectForge.ics) and the SMS interface (/export/sms).

The menu visibility (projectforge.menu.visibility.*) is something else: it hides menu entries for groups of users only, the functions stay reachable.

3.4. Mobile app

The mobile app logs in in the system browser (password and second factor as in the web) and then gets a token of its own. Settings in projectforge.properties:

# Days without any request after which the token of an app is invalid (default 90).
projectforge.app.tokenExpiryDays=90
# Cookies of an SSO proxy in front of ProjectForge, handed to the app (see below).
projectforge.app.proxyCookies=authentik_proxy_*
# Push messages (iPhone), see below.
projectforge.push.apns.keyFile=/path/to/AuthKey_ABC1234567.p8
projectforge.push.apns.keyId=ABC1234567
projectforge.push.apns.teamId=DEF1234567
projectforge.push.apns.topic=de.projectforge.mobileapp

3.4.1. Behind an SSO proxy

If an SSO proxy (e.g. Authentik as forward auth in nginx) stands in front of ProjectForge, it lets only requests with its own session cookie through. The browser has this cookie, the app doesn’t. projectforge.app.proxyCookies names the cookies of the proxy, separated by commas; a trailing matches a prefix (authentik_proxy_). During the login in the browser the server takes these cookies from the request and hands them to the app together with its token. The app sends them with every request and puts them into its web views.

  • Name only the cookies of the proxy: a bare * is ignored, and the cookies of ProjectForge itself are never handed over. A cookie the browser sends twice (e.g. one set by another subdomain) is not taken.

  • If the proxy refuses the app (its session expired), the app logs in again in the browser, which brings fresh cookies and needs a second factor of ProjectForge. Therefore the app has to log in again after each expiry of the proxy session (e.g. the token validity of the Authentik provider).

  • The app gets the proxy session of the browser, not one of its own. With forward auth on domain level it can reach every application behind the proxy. Logging out the app in "My account" does not end the proxy session, and a logout at the proxy in the browser shuts the app out as well.

3.4.2. Push messages

A new notification can wake the app with a push message through Apple’s push service (APNs). The server sends it itself: it needs an APNs authentication key (AuthKey_<keyId>.p8) of the Apple developer account the app belongs to (keyFile, keyId, teamId, and the bundle id of the app as topic). Without keyFile no push is sent, and the app asks for new notifications from time to time as before.

The key belongs to the publisher of the app. The pushes therefore work for installations that have the publisher’s key or their own build of the app. The message shows the title and the beginning of the notification, as the web version does, plus the link and the number of open notifications. Apple sees this payload: each user can switch the content off ("My account", Apps, and the settings of the app; a generic text then, and the app loads the content itself), and projectforge.push.content=false keeps the content out of every push. Chat messages appear only where they appear in mails too. Debug builds of the app use the sandbox of APNs, release builds the production service.

3.5. Customization

3.5.1. Logo and title in pdf outputs

Under Administration → Configuration you can enter your organization name, which is used e.g. as headline of the pdf outputs.

Logos are configured in projectforge.properties. The files must be located in resources/images of the home directory (or be given as absolute paths):

1
2
3
4
5
6
7
8
# Logo of the web app:
projectforge.logoFile=logo.png
# Optional variant for the dark mode:
projectforge.logoFileDark=logo-dark.png
# Optional height of the web app logo in px (12 to 48, default: up to 36):
projectforge.logoHeight=28
# Logo used in exports:
projectforge.export.logoFile=logo-export.png

3.5.2. Customizing i18n translations

ProjectForge allows you to customize or override any translation text displayed in the user interface. This is useful if you want to adapt terminology to your organization’s specific needs.

To customize translations:

  1. Create one or more properties files in the ~/ProjectForge/resources directory:

    • CustomerI18nResources.properties - for default/English translations

    • CustomerI18nResources_de.properties - for German translations

    • CustomerI18nResources_en.properties - for English translations (explicit)

  2. Add your custom key-value pairs to override existing translations or add new ones:

    1
    2
    3
    4
    
    # Example: Override existing translation
    menu.addressList=Contacts
    # Example: Add custom translation
    myorganization.custom.greeting=Welcome to Acme Corp
    
  3. Restart ProjectForge to apply the changes.

Your custom translations will take precedence over the default translations included in ProjectForge.

To find available i18n keys that you can override, refer to:

  • Main application translations: I18nResources.properties

  • Plugin translations: Search the GitHub repository for *I18nResources.properties files in the plugin directories, e.g.:

    • plugins/org.projectforge.plugins.banking/src/main/resources/BankingI18nResources.properties

    • plugins/org.projectforge.plugins.skillmatrix/src/main/resources/SkillMatrixI18nResources.properties

    • And other plugin-specific translation files

3.6. Configuration settings (web interface)

Under Administration → Configuration you can set further parameters of ProjectForge® such as the default time zone, the country phone prefix or cost-unit accounting. Each parameter is explained there. Structured settings are JSON values, e.g. the holidays (additional ones and changes of the built-in ones), the JIRA servers, the contract types and the accounting settings including the business assessment (see business assessment / BWA).

Administration → System offers among others: re-reading the configuration parameters, exporting the 2FA configuration, re-indexing the search index and system integrity checks.

3.7. Using ttf fonts for PDF render engine

Apache FOP is used as internal render engine for PDF outputs. Additional fonts are placed in resources/fonts of the home directory (or the directory configured by projectforge.fontsDirectory). Please refer to the FOP documentation for details on fonts.

3.8. HTML formatted E-Mail templates

ProjectForge works with Groovy templates for generating e-mails (e.g. notifications). You’ll find the built-in templates in the sources under projectforge-business/src/main/resources/mail. To modify a template, copy it to the same relative path in the resources directory of your home directory, e.g. ~/ProjectForge/resources/mail/orderChangeNotification.html, and edit it there. ProjectForge looks there first and falls back to the built-in template if the file doesn’t exist.

The templates are internationalized with the locale of the receiving user.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<html>
#INCLUDE{mail/mailHead.html}
<body>
#INCLUDE{mail/mailOpening.html}
<br />
Here you can place content (don't forget to use i18n keys for supporting internationalized e-mails).
...
<br />
<%= pf.getString("link") %>: <a href="${requestUrl}">${requestUrl}</a>
<br />
This is an example of using dynamic data (here a list of positions will be printed):
<table>
  <% for (position in order.positions) { %>
    <tr>
      <th><%= pf.getString("title") %></th>
      <td>${position.title}</td>
      ...
    </tr>
  <% } %>
</table>

#INCLUDE{mail/mailHistoryTable.html}

#INCLUDE{mail/mailClosing.html}
#INCLUDE{mail/mailFooter.html}
</body>
</html>

3.9. Sending e-mails

ProjectForge sends notifications, password reset mails, 2FA codes and the error digest via SMTP:

1
2
3
4
5
6
7
8
9
mail.session.pfmailsession.emailEnabled=true
mail.session.pfmailsession.standardEmailSender=projectforge@acme.com
# Plain, StartTLS or SSL:
mail.session.pfmailsession.encryption=StartTLS
mail.session.pfmailsession.smtp.host=mail.acme.com
mail.session.pfmailsession.smtp.port=587
mail.session.pfmailsession.smtp.auth=true
mail.session.pfmailsession.smtp.user=projectforge
mail.session.pfmailsession.smtp.password=<password>

3.10. Support error digest

If a support address is configured (projectforge.support.mail) and sending mails is set up, ProjectForge collects the errors of a period and sends them to this address as one digest mail per interval. Nothing is sent for a period without errors. The digest collects:

  • unexpected errors in requests of logged-in users (no user mistakes such as validation or access errors),

  • logged errors (level ERROR),

  • external systems that aren’t reachable (LDAP, IdP, gateway, SMS/phone services, SMTP, …​), recognized by connection errors and timeouts, also from warnings,

  • sync runs that failed, were aborted or timed out since the last digest.

Equal errors (differing only in ids, numbers or quoted values such as a user name) are aggregated. If a new version groups them more coarsely, the problems recorded before are merged shortly after the start (counts summed, the admin’s status kept if they agree, else new). The mail lists new problems, regressions (problems that occurred again after being resolved) and spikes (much more often than in the last 7 days) first, then the 30 most frequent known problems sorted by severity (unreachable external systems first). Each problem links to the System dashboard (Administration), where it can be acknowledged, resolved, ignored or muted; ignored and muted problems are only counted. The attached text file contains up to five single occurrences per group with user, request and stack trace.

For an analysis or classification with an AI (or any other tool), the digest also attaches everything as a JSON file (format: projectforge-log-analysis), and the System dashboard downloads all problems of its current filter in the same format (Download as JSON), with sample message, stack trace and the occurrences of the last 30 days. The file explains its fields and values itself (guide), so it can be handed over as it is. It contains no user names, but messages and requests are as logged and may contain personal data: check this before passing it to an external service.

A new security or data problem (or one that occurred again after being resolved) doesn’t wait for the end of the interval: it sends the digest early, as soon as it reaches its threshold, but at most every urgentMinGap (default 1 hour), also at night. By default the regular digest is sent once a day at 07:00.

Each problem goes to the recipients of its audience: developers (bugs, data problems), admins (external systems, configuration, sync runs) or security. Without recipients of its own, an audience gets the digest at projectforge.support.mail.

File: projectforge.properties
projectforge.support.mail=support@acme.com
# How often the regular digest is sent, e.g. 15m, 1h, 1d. 0 disables the digest. Default: 1d
projectforge.support.errorDigest.interval=1d
# Time of day (HH:mm) of the regular digest if the interval is at least a day. Blank: counted from the last digest. Default: 07:00
projectforge.support.errorDigest.sendAt=07:00
# Minimum gap between early digests (urgent problems), e.g. 5m, 1h. Default: 1h
projectforge.support.errorDigest.urgentMinGap=1h
# Optional, comma separated, default: projectforge.support.mail
projectforge.support.errorDigest.recipients.developer=dev@acme.com
projectforge.support.errorDigest.recipients.admin=ops@acme.com
projectforge.support.errorDigest.recipients.security=security@acme.com

3.11. System dashboard and log viewer

Administrators find the System dashboard in the Administration menu. It shows the problems collected for the error digest (see above), the runs of the scheduled jobs and offers a log viewer for searching the log files, including the context of a log entry. The counts are stored in the database (projectforge.support.logAggregation. and projectforge.scheduler.stats. in application.properties, cleaned up by the daily privacy protection job).

3.12. Phone calls and texting with ProjectForge

3.12.1. Phone calls

If configured, users are able to initiate phone calls directly by clicking on a phone number of an address or by entering a name or a phone number on the direct call page. Calls are initiated via Sipgate:

1
2
3
4
5
6
projectforge.sipgate.baseUri=<base uri of the Sipgate REST API>
projectforge.sipgate.tokenId=<token id>
projectforge.sipgate.token=<token>
# Device used for calls, if the user hasn't configured own Sipgate devices (e.g. "p2"):
projectforge.sipgate.defaultDevice=
projectforge.sipgate.basePhoneNumber=
image

Figure 1:

You can initiate a phone call by simply clicking on a phone number of an address or by entering a name or number into the number field.

Don’t forget to configure your default country phone prefix under Administration → Configuration.

3.12.2. Texting

If an SMS gateway is configured, users are able to send text messages by clicking on the SMS symbol of an address or by entering a name or a cell phone number. ProjectForge calls your gateway by http; #number and #message are replaced by the user’s input. The text messages are also used as second factor (2FA), if configured.

1
2
3
4
5
6
7
projectforge.sms.url=https://smsgateway.acme.com/api.php
# GET (default) or POST:
projectforge.sms.httpMethod=GET
projectforge.sms.httpParameters={username:'projectforge',text:'#message',to:'#number'}
# Patterns of the gateway's response:
projectforge.sms.returnCodePattern.success=^OK.*
projectforge.sms.returnCodePattern.error=^Error.*
image

Figure 2:

You can send text messages by simply clicking on the SMS symbol of an address or by entering a name or cell phone number.

All options are listed in application.properties (projectforge.sms.*).

3.12.3. Phone lookup

With the phone lookup service, a telephony system (such as Asterisk) may look up the caller of incoming phone calls: projectforge.acme.com/rsPublic/phoneLookup?nr=<number>&key=<key>;. Wrong keys are delayed like failed logins.

The service is disabled as long as no key is configured (the default is empty). For enabling it, set your own long random secret key in projectforge.properties (e.g. generated with openssl rand -hex 32):

1
projectforge.phoneLookupKey=<your-random-secret>

3.13. Digital business cards

Users create their business cards under My business cards in the user menu: a QR code with the contact data (vCard) taken from an address. Two optional features need configuration.

3.13.1. Photo

A photo is not part of the QR code but a public url (/rsPublic/contactImage/<token>, no login) in its vCard; it is only offered for the user’s own address. The token is random (256 bit) and belongs to the address, so the url can’t be guessed and stays the same when the image is replaced or deleted (without an image it answers 404; browsers may cache the old image for up to an hour). Failed attempts are throttled per client address. Users revoke the url with Renew photo link on the card (all cards of the address then get a new one); switching off the photo of a card does not revoke it. The images are delivered by the main instance and by the gateway (the token is pushed with the address), under the same url. Update the gateway together with the main instance: an old gateway does not know the new path and answers 404 for the photos.

The base of the url is, in this order: projectforge.businessCard.photoBaseUrl (overrides everything, e.g. a reverse proxy), the public url of the gateway (projectforge.gateway.push.publicUrl, see External Gateway mode), the domain of the instance (projectforge.domain, not if it is localhost). Without any, there is no photo url. Add a limit_req for /rsPublic/contactImage/ to the reverse proxy.

3.13.2. Apple Wallet

With a Pass Type ID certificate each card can be added to the Apple Wallet (a .pkpass file, no app needed). The certificate stays on the main instance, never on the gateway. Without the settings the button is not shown.

Set up once per installation, with an Apple Developer account:

  1. Developer portal, Certificates, Identifiers & Profiles, Identifiers, Pass Type IDs: create a Pass Type ID, e.g. pass.com.acme.projectforge.businesscard. It is independent of the signing certificates of apps.

  2. In this Pass Type ID: Create Certificate with a certificate signing request, download pass.cer.

  3. Import pass.cer into the keychain (next to the private key of the request) and export both as pass.p12.

  4. Download the intermediate certificate Apple Worldwide Developer Relations - G4 (apple.com/certificateauthority).

  5. Configure it in projectforge.properties:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
wallet.passcertificate.path=/etc/projectforge/wallet/pass.p12
wallet.passcertificate.password=<password of the p12>
wallet.wwdrcertificate.path=/etc/projectforge/wallet/AppleWWDRCAG4.cer
wallet.teamIdentifier=<team id of the developer account>
wallet.passTypeIdentifier=pass.com.acme.projectforge.businesscard
wallet.organizationName=ACME GmbH
# Optional: colors (rgb(r, g, b)) and logo (png) of the pass
wallet.backgroundColor=rgb(255, 255, 255)
wallet.foregroundColor=rgb(33, 37, 41)
wallet.labelColor=rgb(108, 117, 125)
wallet.logoFile=/etc/projectforge/wallet/logo.png

The certificate is valid for about one year. Before it expires, create a new one for the same Pass Type ID (steps 2 and 3) and replace pass.p12; passes already in a Wallet keep working. A newer .p12 is read at the next start. An issued pass is not updated afterwards: a changed card has to be added again.

3.14. Number ranges of customers, projects and cost units

Customers and projects have their own numbers, every project a system-wide unique project number. Number ranges (Nummernkreise) define which customer and project numbers belong together and how the cost units (Kost2) of a project are derived from its number. They are configured in projectforge.properties (projectforge.fibu.number-ranges.*). If you don’t use cost-unit accounting, or don’t need Kost2 of projects, no configuration is needed: customer and project numbers are free then.

3.14.1. Which ranges apply

On start, ProjectForge logs which ranges apply (Number ranges: mode …​):

Mode Applies if

CONFIGURED

At least one range is configured. Only the configured ranges apply.

LEGACY

No range is configured, but Kost2 of projects in the formerly built-in ranges exist: 4 for internal projects (4...) and 5 for customers 0-999 with up to 100 projects each (5...). These two ranges then apply as before. This is detected on every start and not stored.

FREE

Otherwise (e.g. a new installation). Customer and project numbers are free, projects get no Kost2. Kost2 without a project can still be created.

If an installation in mode LEGACY gets its first configured range, only the configured ranges apply. Then configure the former ranges 4 and 5 as well (see the example below).

3.14.2. Settings

Each range is a list entry projectforge.fibu.number-ranges.ranges[n].<property>:

Property Default Description

prefix

First digit (1-9) of the project and Kost2 numbers of this range, unique for all ranges.

name

Display name, e.g. Intern.

customer-min, customer-max

Lowest and highest customer number of this range; give both or none. A range without customers is for projects without customer (e.g. internal projects); its project-digits must be 5.

project-digits

2

Number of digits of the project number per customer (1-5), or of the whole range for a range without customers.

segments

1,3,2,2

Segments of the 8-digit Kost2 for display and input, e.g. 1,4,1,2 for 1.#... Must start with 1 and end with 2 (the Kost2 type) and sum up to 8.

Global settings:

Property Default Description

projectforge.fibu.number-ranges.allow-free-numbers

true

If false, every customer number must lie in a configured range (protects against typos) and a project needs a customer of a range or belongs to the range without customers; applies to the former ranges 4 and 5 as well.

projectforge.fibu.number-ranges.default-segments

1,3,2,2

Segments of the Kost2 whose first digit belongs to no range (Kost2 without project).

3.14.3. Project and Kost2 numbers

A project of a range gets a 6-digit project number, its Kost2 are this number followed by the 2-digit Kost2 type:

project number = prefix * 100000 + (customer mod 10^(5 - project-digits)) * 10^project-digits + project
project number = prefix * 100000 + project                      (range without customers)
Kost2          = project number * 100 + Kost2 type

The customer number itself is part of the project number: its last 5 - project-digits digits, whatever customer-min is. Example: range 1 with the customers 10000-19999 (or 10001-19999), 1 project digit and the segments 1,4,1,2. Project 3 of customer 10007 has the project number 100073 (shown as 1.0007.3), its Kost2 of type 02 is 1.0007.3.02. Customer 123 of range 5 (customers 0-999, 2 project digits) gets 5.123.xx.

So all customers of a range must share the digits in front of those last digits: a range with 2 project digits holds at most the 1000 customers of one block like 0-999 (100 projects per customer), a range with 1 project digit at most the 10000 customers of one block like 10000-19999.

File: projectforge.properties
# Former range 4: internal projects 4.###.##.##
projectforge.fibu.number-ranges.ranges[0].prefix=4
projectforge.fibu.number-ranges.ranges[0].name=Intern
projectforge.fibu.number-ranges.ranges[0].project-digits=5
# Former range 5: customers 0-999 with up to 100 projects each, 5.###.##.##
projectforge.fibu.number-ranges.ranges[1].prefix=5
projectforge.fibu.number-ranges.ranges[1].customer-min=0
projectforge.fibu.number-ranges.ranges[1].customer-max=999
projectforge.fibu.number-ranges.ranges[1].project-digits=2
# Customers 10000-19999 with up to 10 projects each, 1.####.#.##
projectforge.fibu.number-ranges.ranges[2].prefix=1
projectforge.fibu.number-ranges.ranges[2].customer-min=10000
projectforge.fibu.number-ranges.ranges[2].customer-max=19999
projectforge.fibu.number-ranges.ranges[2].project-digits=1
projectforge.fibu.number-ranges.ranges[2].segments=1,4,1,2
# Optional: refuse customer numbers outside these ranges
projectforge.fibu.number-ranges.allow-free-numbers=false

3.14.4. Notes

  • An inconsistent configuration (prefix used twice, overlapping customer numbers, too many customers for the project digits, invalid segments) stops the start with a message naming the range.

  • New customers and projects are offered the next free number of their range.

  • A project whose customer belongs to no range gets a free project number, which must not lie in the project numbers of a range (e.g. 100000-199999 for prefix 1). Such projects and projects in mode FREE can’t get Kost2.

  • Kost2 without a project must not start with the prefix of a range: the Kost2 of a range are those of its projects.

  • Stored project and Kost2 numbers are not renumbered if you change a range later. Existing Kost2 stay valid even if their range is no longer configured, but plan new ranges (customer numbers and project digits) before using them.

  • Invoicing quota (projectforge.invoicing-quota.): the patterns apply to the Kost2 numbers as their range segments them (e.g. 1. for 1.#..). With projectforge.invoicing-quota.billed-by-invoiced-kost2-types=true the time booked on Kost2 of customer projects with an invoiced Kost2 type counts as billed, independent of the digits.

3.15. Group nomenclature

The configuration parameter Group nomenclature (groupNomenclature, category finance) describes how groups are named. It is a description, not a rule: the project and structure wizards suggest the names of new groups following it, and its description is shown at the name of a group and in the wizards; no name is checked against it.

  • Description: rich text explaining the convention.

  • Pattern of the base name of a project’s groups, with the placeholders {customer.identifier}, {customer.name}, {customer.number}, {project.identifier}, {project.name} and {project.number}. An empty identifier is replaced by the name. Empty: the project’s name.

  • Suffixes of the manager, team and externals' groups, appended with a hyphen. Empty: the wizard’s default (pm, none, extern/external).

  • Letter case of the whole suggested name: unchanged, lower case or upper case.

Example: pattern {customer.identifier}-{project.identifier}, suffixes pm and extern, lower case suggest acme-portal-pm, acme-portal and acme-portal-extern. The structure wizard uses the pattern if the picked element belongs to a project, otherwise the element’s title.

3.16. business assessment / BWA

If you work with accounting records (DATEV) you may want to have business assessments (BWA in German). They are configured in the configuration parameter Accounting (fibu.accounting, element businessAssessment). The business assessments of DATEV consist of rows. Each row has a line number (no) and a title (for displaying). The accountRange defines which DATEV accounts match the row. Please note that a record is assigned to the first row found.
If the priority of a row is not high, the row will not be shown in the short form of business assessments. The value defines row amounts which are calculated: a formula of row ids starting with = or a Groovy script returning the amount. The account number ranges of debitors and creditors are part of this parameter, too. Example:

{
  "debitorsAccountNumberRanges": "10000-12999",
  "creditorsAccountNumberRanges": "70000-79999",
  "businessAssessment": {
    "heading": "BWA",
    "overallPerformance": "gesamtleistung",
    "merchandisePurchase": "matWareneinkauf",
    "preliminaryResult": "vorlaeufigesErgebnis",
    "rows": [
      {"no": "1020", "id": "umsatzErloese", "accountRange": "4000-4799", "priority": "MIDDLE", "title": "Umsatzerlöse", "indent": 1},
      {"no": "1051", "id": "gesamtleistung", "value": "=umsatzErloese", "priority": "HIGH", "title": "Gesamtleistung"},
      {"no": "1060", "id": "matWareneinkauf", "accountRange": "5700-5999", "priority": "HIGH", "title": "Mat./Wareneinkauf"},
      {"no": "1380", "id": "vorlaeufigesErgebnis", "value": "=gesamtleistung+matWareneinkauf", "priority": "HIGH", "title": "Vorläufiges Ergebnis"},
      {"id": "relativePerformance", "value": "if (gesamtleistung != 0.0) { return vorlaeufigesErgebnis / gesamtleistung } else { return 0.0 }", "priority": "HIGH", "title": "relative Performance", "scale": 4}
    ]
  }
}

3.17. SecurityConfig

3.17.1. Secure passwords

Passwords are never stored in clear text: every password is hashed together with a random salt per user and a pepper (see below). The minimum password length and the number of character classes a password must contain (uppercase letters, lowercase letters, digits, special characters, e.g. 3 of 4) are configured under Administration → Configuration.

3.17.2. Protection against brute force attacks

If a login fails (via login page or REST service), a penalty time is set for the user name and for the client’s IP address. With every further failed login the penalty grows, and during the penalty no new attempt is accepted. The penalty for IP addresses grows much slower, so many users behind the same NAT aren’t locked out. Please refer to the source code (LoginProtection) for details.

The client’s IP address is taken from X-Forwarded-For only for requests coming from a trusted proxy (projectforge.security.trustedProxies, default: localhost and the private networks).

3.17.3. Salted passwords with pepper

A pepper string is configured in projectforge.properties; the setup wizard generates a random one. Before hashing a password, the pepper is concatenated to the salt and the password, so a database dump alone isn’t enough for a brute force attack.

1
projectforge.security.passwordPepper=*******SECRET********

Keep this value safe: if you lose or change the pepper, no user with a password hashed with the former pepper can log in any more. The same applies to projectforge.security.authenticationTokenEncryptionKey, which encrypts the users' authentication tokens (changing it only invalidates the tokens, not the passwords).

3.17.4. 2FA (two-factor-authentication)

2FA (two factor authentication) is supported by:

  • authenticator apps (TOTP, RFC 6238), e.g. Google or Microsoft Authenticator,

  • WebAuthn/FIDO2 (security keys such as YubiKey),

  • codes sent by e-mail or text message (SMS, if configured).

Each user sets up their second factors under My account.

You specify for which URLs a second factor is required and how long a successful 2FA is valid, using shortcuts for predefined sets of URI matchers and/or own regular expressions. A web filter checks for every request whether a 2FA is required and whether the last successful 2FA of the current session is older than the specified expiry period. The timestamp of a successful 2FA is stored in the user’s session as well as in the user’s stay-logged-in session.

Recommended example configuration:

File: projectforge.properties
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
projectforge.2fa.expiryPeriod.minutes1=
projectforge.2fa.expiryPeriod.minutes10=PASSWORD
projectforge.2fa.expiryPeriod.hours1=ADMIN_WRITE;MY_ACCOUNT
projectforge.2fa.expiryPeriod.hours8=ADMIN;SCRIPT_WRITE;FINANCE_WRITE;ORGA_WRITE;HR_WRITE
projectforge.2fa.expiryPeriod.days30=HR;FINANCE;ORGA;SCRIPT
projectforge.2fa.expiryPeriod.days90=
# Number of days a 2FA is required, when stay-logged-in is used. Without stay-logged-in and given, a 2FA is required
# after every login.
projectforge.2fa.loginExpiryDays=30
# Optional: no e-mail as second factor for members of these groups (comma separated):
projectforge.2fa.disableMail2FAForGroups=PF_Admin

Rules

  • An expression starting with / is an absolute path of the URI and is matched as a prefix: /rs/user → ^/rs/user.*, so any URI starting with /rs/user matches.

  • Empty entries mean: no second factor.

  • Regular expressions and shortcuts are separated by ;.

  • Supported shortcuts are ALL, ADMIN, ADMIN_WRITE, FINANCE, FINANCE_WRITE, HR, HR_WRITE, ORGA, ORGA_WRITE, SCRIPT, SCRIPT_WRITE, MY_ACCOUNT and PASSWORD, each containing a bunch of regular expressions.

  • You may also use WRITE:<entity-name> for specifying only the write access calls of an entity (REST calls). Reading of entities / detail view is not affected. For specifying all entity activities, use /rs/<entity-name> instead.

  • You may also define own expressions, such as: ADMIN;/rs/report;WRITE:user

  • If an URI matches several periods, the shortest one is used.

  • The former Wicket frontend (/wa/…​) is gone; its URLs are only redirected into the new frontend, so expressions with /wa/ don’t protect anything any more. Replace them by the /rs/…​ paths of the entities.

Please use Administration → System → Export 2FA configuration for verifying your configuration.

3.18. LDAP

3.18.1. Requirements

The LDAP must support the following additional schemas (example of slapd.conf of OpenLDAP):

1
2
include /etc/openldap/schema/cosine.schema
include /etc/openldap/schema/inetorgperson.schema

3.18.2. Configuration

The groupBase setting is only needed in master mode. The managerUser is not needed in simple slave mode. For further information see below.

File: projectforge.properties
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
...
projectforge.ldap.server=
projectforge.ldap.baseDN=
projectforge.ldap.managerUser=
projectforge.ldap.managerPassword=
projectforge.ldap.port=
projectforge.ldap.sslCertificateFile=
projectforge.ldap.groupBase=
projectforge.ldap.userBase=
projectforge.ldap.authentication=
projectforge.ldap.posixAccountsDefaultGidNumber=
projectforge.ldap.sambaAccountsSIDPrefix=
projectforge.ldap.sambaAccountsPrimaryGroupSID=
...

3.18.3. Local (admin) users

Please log in as administrator with the stay-logged-in option of the login page (no LDAP configured yet). Restart ProjectForge with LDAP configured and use the same browser. You should be logged in. Edit every user who should be authenticated against ProjectForge (instead of LDAP), check the option local user and save. Now the local users may log-in without LDAP credentials. This is especially use-ful for administrator users of ProjectForge.

3.18.4. LDAP over SSL

openssl s_client -showcerts -connect ldap.acme.internal:636 > ldap.pem Copy the lines including -----BEGIN CERTIFICATE----- until -----END CERTIFICATE----- (including) to a file called ldap.cert. Now you can configure the certificate file in projectforge.properties or you can import the certificate to your java keystore. The first solution is independent of your JAVA version and is recommended first.

3.18.5. ProjectForge as LDAP master

The LDAP master mode is the most tested LDAP mode of ProjectForge and is used in production by us. In this mode, ProjectForge needs a LDAP user with write access to groups and users. The ProjectForge user’s will be synchronized in LDAP. If there is any conflict between LDAP and ProjectForge, the ProjectForge settings are used. The configuration of this mode is simple:

File: projectforge.properties
1
2
3
...
projectforge.login.handlerClass=LdapMasterLoginHandler
...
  1. This LDAP login handler has read-write access to the LDAP server and acts as master of the user and group data. All changes of ProjectForge’s users and groups will be written through.

  2. Any change of the LDAP server will be ignored and may be overwritten by ProjectForge.

  3. Passwords
    After each successful login at ProjectForge (via the login page) ProjectForges tries to authenticate the user with the given username/password credentials at LDAP. If the LDAP authentication fails ProjectForge changes the password with the actual password of the user (given on the login page). If the user changes their password (Change password), the LDAP password will be changed respectively. In LDAP master mode, the user is also able to change their "WLAN/Samba password" (Change WLAN/Samba password). The password entered there, will be written to the LDAP attribute "sambaNTPassword".

  4. Deactivated users
    Deactivated users will be moved to an sub userbase called "deactivated". The e-mail will be invalidated and the password will be deleted. Deleted and deactivated users are removed from any LDAP group. After reactivating the user, the password is set again when the user logs in the next time via the login page.

  5. Deleted Users
    Deleted users will not be synchronized and removed in LDAP if exist.

  6. New users
    New users (created in ProjectForge’s user edit page) will be created first without password in the LDAP system directly but with "WLAN/Samba password". Such users need to log-in first at ProjectForge, otherwise their LDAP passwords aren’t set (no log-in at any other system connecting to the LDAP is possible until the first log-in at ProjectForge).

  7. Modifying fields in LDAP
    ProjectForge does only modify supported fields of known LDAP users. If any modification is done in LDAP of fields and users/groups known by ProjectForge they will be overwritten by ProjectForge during the next synchronization run. If any modification in other fields of LDAP users are done, they should be left untouched by ProjectForge.

  8. Synchronization
    After any modification of any ProjectForge user or group, the synchronization is started for all LDAP users and groups. If no modification is done, the synchronization is done approximately hourly, but only if any user activity is detected in ProjectForge. You can force a synchronization by restarting ProjectForge.

  9. Groups and assigned users
    Users will be added as uniqueMember entries in the LDAP groups if also assigned in ProjectForge to the same ProjectForge group.

LDAP entries

See figures 3 and 4 for detailed information of how does ProjectForge stores and expect LDAP fields and object classes.

image

Figure 3:

ProjectForge uses the fields and object classes shown in this figure for users. The object class posixAccount with the fields uidNumber, gidNumber, homeDirectory and loginShell are only used and managed if configured). The field employeeNumber is used by ProjectForge for storing the primary key of the ProjectForge user entry in LDAP (for a better synchronization). So the renaming of user names should work.

image

Figure 4:

ProjectForge uses the fields and object classes shown in this figure for groups. The field businessCategory is used by ProjectForge for storing the primary key of the ProjectForge group entry in LDAP (for a better synchronization). So the renaming of group names should work.

Managing POSIX accounts

ProjectForge supports the management of POSIX accounts with the data uidNumber, gidNumber, homeDirectory, and loginShell.

You can administer the POSIX account data in the user edit page after configuring projectforge.properties like the above example. Please refer to the tool-tips for further information:

image

Figure 5:

ProjectForge is able to manage POSIX accounts in LDAP master mode.

  • After clicking the Button create beside the UID number field ProjectForge will prefill all POSIX account fields:

    1. The UID number will be the next free number (ProjectForge searches the highest used UID and increments it by one). If no such number found, 1,000 is assumed.

    2. The GID number will be preset with projectforge.ldap.posixAccountsDefaultGidNumber configured in projectforge.properties.

    3. The home directory will be the prefix /home/ followed by the user-name: e. g. /home/jdoe (not configurable).

    4. The login-shell will be preset with /bin/bash (not configurable).

      Don’t forget to save the user. After changing any of these POSIX account fields the LDAP entries are updated immediately.

  • ProjectForge presets a new UID number automatically (see above).

  • ProjectForge assures that no UID number is given twice.

  • Every LDAP entry gets the object-class posixAccount only if the POSIX account data were created in ProjectForge for this user.

3.18.6. Managing Samba accounts

File: projectforge.properties
1
2
3
4
...
projectforge.ldap.sambaAccountsSIDPrefix=
projectforge.ldap.sambaAccountsPrimaryGroupSID=
...
image

Figure 6:

ProjectForge is able to manage POSIX and Samba accounts in LDAP master mode.

  • After clicking the Button create beside the UID number field ProjectForge will prefill all SAMBA account fields:

    1. The UID number will be the next free number (ProjectForge searches the highest used UID and increments it by one). If no such number found, 1,000 is assumed.

    2. The GID number will be preset with the defaultSambaPrimaryGroupSID configured in projectforge.properties.

    3. Parallel usage with POSIX, which is recommended:
      Please click the 'create' button near to the UID of the POSIX account, the suffix of SAMBASID will be the same UID. ProjectForge’s tries first to get the same UID for SAMBA as for POSIX if no conflict was found.

      Don’t forget to save the user. After changing any of these Samba account fields the LDAP entries are updated immediately.

  • Every LDAP entry gets the object-class sambaSamAccount only if the Samba account data were created in ProjectForge for this user.

3.18.7. ProjectForge as LDAP client

This LDAP login handler acts as a LDAP slave, meaning, that LDAP will be accessed in read-only mode. There are 3 modes available: simple, users and users-groups. The configuration of this mode is simple:

File: projectforge.properties
1
2
3
...
projectforge.login.handlerClass=LdapSlaveLoginHandler
...
Simple mode

The simple mode is assumed if no ldap managerUser is given in the projectforge.properties.

  • Simple means that only username and password is checked, all other user settings such as assigned groups and user name etc. are managed by ProjectForge.

  • No ldap user is needed for accessing users or groups of LDAP, only the user’s login-name and password is checked by trying to authenticate!

  • If a user is deactivated in LDAP the user has the possibility to work with ProjectForge unlimited as long as he uses his stay-logged-in-method! (If not acceptable please use the normal user mode instead.)

  • For local users any LDAP setting is ignored.

Users mode

The normal user mode is assumed if a LDAP managerUser is given in the projectforge.properties.

  • Normal means that username and password is checked and all other user settings such as user name etc. are read by a given LDAP manager user.

  • If a user is deleted in LDAP the user will be marked as deleted also in ProjectForge’s data-base. Any login after synchronizing isn’t allowed (the stay-logged-in-feature fails also for deleted users).

  • For local users any LDAP setting is ignored.

  • All known LDAP user fields of the users are synchronized (given name, surname, e-mail etc.).

Users-groups mode

This mode is not yet implemented, the users-mode is used instead.

3.18.8. ProjectForge without LDAP

You can also use ProjectForge without LDAP (default).

File: projectforge.properties
1
2
3
...
projectforge.login.handlerClass=LoginDefaultHandler
...

3.19. OAuth2 / OIDC Authentication

ProjectForge supports authentication via an external OAuth2/OIDC provider. Users log in through the identity provider (e.g. Authentik, Keycloak) and are redirected back to ProjectForge. The user must already exist in ProjectForge (matched by idpExternalId or username).

3.19.1. Configuration

Add the following OAuth2/OIDC client configuration to projectforge.properties:

spring.security.oauth2.client.registration.authentik.client-id=${AUTHENTIK_CLIENT_ID}
spring.security.oauth2.client.registration.authentik.client-secret=${AUTHENTIK_CLIENT_SECRET}
spring.security.oauth2.client.registration.authentik.scope=openid,profile,email
spring.security.oauth2.client.registration.authentik.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.authentik.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.provider.authentik.issuer-uri=${AUTHENTIK_ISSUER_URI}

The registration id must be authentik, also for other providers such as Keycloak: ProjectForge activates its OAuth2 login only if spring.security.oauth2.client.registration.authentik.client-id is set. For Keycloak, just set the issuer URI of your realm (e.g. auth.acme.com/realms/acme.com) and the client of ProjectForge. The redirect URI to register at the provider is <your-projectforge-domain>/login/oauth2/code/authentik.

The login page of ProjectForge has no button for the identity provider: users start the login via <your-projectforge-domain>/oauth2/authorization/authentik (e.g. as a link or bookmark, or from the application dashboard of your identity provider).

3.19.2. User mapping

On successful OAuth2 login, ProjectForge resolves the local user account as follows:

  1. Match the OIDC sub claim against the idpExternalId field of ProjectForge users

  2. If no match is found, fall back to matching the preferred_username claim against the ProjectForge username

  3. If still no match: login is rejected

Deactivated users are always rejected regardless of a valid OAuth2 session.

3.19.3. External Gateway mode

ProjectForge can run as a reduced, public-facing instance that authenticates exclusively via OAuth2/OIDC. In this mode only the following services are exposed:

  • CardDAV (address book sync)

  • CalDAV / ICS subscriptions (calendar export)

  • DataTransfer (file exchange areas)

Activate with the Spring profile external-gateway:

--spring.profiles.active=external-gateway
Minimal gateway configuration

The gateway instance’s projectforge.properties requires the gateway mode and the database connection. All other gateway settings (CardDAV, the functions switched off, see Switching off functions) are provided by the external-gateway profile automatically. Without the profile, the gateway mode switches off all functions but the data transfer as well.

The OAuth2/OIDC configuration is only needed for the DataTransfer UI: CardDAV and CalDAV/ICS authenticate via tokens and work without it. If the OAuth2 client is not configured, the gateway starts without a login option.

ProjectForge adds the home projectforge.properties as --spring.config.additional-location, which Spring loads with the highest priority. It therefore overrides the external-gateway profile, so keep this file limited to the settings below rather than repeating profile defaults.
# Public URL of the gateway, including the port if published directly.
# This is the {baseUrl} of the OAuth2 redirect URI below.
projectforge.domain=https://gateway.example.com

# Gateway mode
projectforge.gateway.enabled=true
projectforge.gateway.sync.secret=<shared-secret>

# PostgreSQL (adjust host/credentials to your setup)
spring.datasource.url=jdbc:postgresql://postgres:5432/projectforge
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.username=projectforge
spring.datasource.password=<your-password>

# Required: the DAV and calendar tokens pushed by the main instance are stored encrypted.
projectforge.security.authenticationTokenEncryptionKey=<your-key>

# OAuth2/OIDC (only required for the DataTransfer UI)
spring.security.oauth2.client.registration.authentik.client-id=${AUTHENTIK_CLIENT_ID}
spring.security.oauth2.client.registration.authentik.client-secret=${AUTHENTIK_CLIENT_SECRET}
spring.security.oauth2.client.registration.authentik.scope=openid,profile,email
spring.security.oauth2.client.registration.authentik.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.authentik.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.provider.authentik.issuer-uri=${AUTHENTIK_ISSUER_URI}

For testing with HSQLDB (no external database needed), replace the PostgreSQL block:

spring.datasource.url=jdbc:hsqldb:file:${projectforge.base.dir}/database/projectforge;shutdown=true
spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver
spring.datasource.username=sa
spring.datasource.password=

The internal ProjectForge instance syncs user data to the gateway via the shared secret.

Main instance push configuration

The main (internal) ProjectForge instance must be configured to push user, group, address and calendar data to the gateway. Add the following to the main instance’s projectforge.properties:

projectforge.gateway.push.enabled=true
projectforge.gateway.push.url=https://gateway.example.com/api/gateway/sync
projectforge.gateway.push.secret=<shared-secret>
projectforge.gateway.push.syncIntervalMs=900000
Property Description

push.enabled

Enable periodic sync push to the gateway instance

push.url

Base URL of the gateway’s sync API endpoint

push.secret

Shared secret (must match projectforge.gateway.sync.secret on the gateway)

push.syncIntervalMs

Interval between sync pushes in milliseconds (default: 900000 = 15 minutes)

push.initialDelayMs

Delay of the first sync push after start-up in milliseconds (default: 120000 = 2 minutes)

push.publicUrl

URL of the gateway for browsers (external links of data transfer areas), only if it differs from push.url without /api/gateway/sync

The push service sends users, groups, address books, and cached ICS calendar data to the gateway. All requests are authenticated via the X-Gateway-Secret HTTP header.

Data transfer areas with external download or upload are administered on the main instance and pushed to the gateway with each sync. Their external link points to the gateway, external users and the internal users with access work with the files there; on the gateway these areas are read-only and keep the ids of the main instance. The gateway has only these areas, it creates none itself (no personal boxes). Switching off the external access or deleting the area on the main instance removes it from the gateway. The gateway sends the notification mails of these areas to their observers (e.g. on an external upload), so it needs a mail server configuration; for this the push includes the email addresses of these observers, and only theirs. With projectforge.datatransfer.externalAccess.localAllowed=true the external access runs on the main instance instead (nothing is pushed), with false it can’t be enabled at all.

3.20. Identity Provider Sync (IdP)

ProjectForge can act as a master for user and group data, provisioning them into an external identity provider. This is independent of the OAuth2 login described above — it handles user/group lifecycle management. Currently supported providers are Keycloak and Authentik.

3.20.1. Configuration

# Provider: "keycloak" (default) or "authentik"
projectforge.idp.provider=keycloak
projectforge.login.handlerClass=IdpMasterLoginHandler

# Optional: sync passwords to IdP on successful login
projectforge.idp.syncPasswords=false

3.20.2. Keycloak setup

Realm and client
  1. Create a realm (e.g. acme.com)

  2. Enable locales globally: de, en

  3. Create a client projectforge (confidential, service account enabled)

  4. Note down the client secret

projectforge.keycloak.serverUrl=https://auth.acme.com
projectforge.keycloak.realm=acme.com
projectforge.keycloak.clientId=projectforge
projectforge.keycloak.clientSecret=<secret>
projectforge.keycloak.pageSize=100
User attributes

Create the following custom user attributes in the Keycloak realm:

Attribute Min Length Max Length Notes

jiraUsername

0

100

mobilePhone

0

255

gender

Options: MALE, FEMALE, DIVERSE, UNKNOWN

locale

0

—

Validated automatically (de, en)

organization

0

255

description

0

255

nickname

0

255

sambaNTPassword

0

255

WLAN password (Samba NT hash)

Set the username validator minimum length to 2 (Keycloak default is 3; reduced for short usernames like jo).

Users must not be allowed to edit their own attributes (name, first name, etc.) in Keycloak.

3.20.3. Authentik setup

  1. Create a new role (e.g. projectforge-sync) with permissions: Group (add, change, delete, view), User (add, change, delete, view, add/remove user to group)

  2. Create a sync user in Admin with this role

  3. Create an API token: Admin → Directory → Tokens and App passwords → Create

projectforge.authentik.serverUrl=https://authentik.acme.com
projectforge.authentik.apiToken=<api-token>
projectforge.authentik.pageSize=100

Authentik stores custom attributes as a JSON object on users/groups — no pre-configuration of attribute schemas is needed.

3.20.4. Attribute mappings

Configure which ProjectForge fields are synced to IdP user/group attributes:

projectforge.idp.userAttributes.jiraUsername=jiraUsername
projectforge.idp.userAttributes.mobilePhone=mobilePhone
projectforge.idp.userAttributes.gender=gender
projectforge.idp.userAttributes.locale=locale
projectforge.idp.userAttributes.organization=organization
projectforge.idp.userAttributes.description=description
projectforge.idp.userAttributes.nickname=nickname

projectforge.idp.groupAttributes.description=description
projectforge.idp.groupAttributes.organization=organization

3.20.5. WLAN password sync (optional)

projectforge.idp.wlanPasswordAttribute=sambaNTPassword

3.20.6. Login handlers

Handler Description

LoginDefaultHandler

No external IdP (default)

IdpMasterLoginHandler

ProjectForge is master; users/groups are pushed PF → IdP

IdpLoginHandler

IdP is master; users/groups are pulled IdP → PF

3.21. Troubleshooting

3.21.1. NullPointerException on pages with diagrams under Linux

If you will receive exceptions while calling pages with diagrams such as under menu project management → my statistics:

Caused by: java.lang.NullPointerException
        at java.desktop/sun.awt.FontConfiguration.getVersion(FontConfiguration.java:1262)

the Linux package fontconfig probably isn’t installed. Please install this package and restart ProjectForge.

4. Maintenance

4.1. Updating

Updating is easy: back up your database, stop ProjectForge and start the new version (new JAR file or docker pull micromata/projectforge and re-create the container). Database schema and data are migrated automatically (Flyway) on the first start. Please check the Migration guide for the few changes that need your attention.

4.2. Re-indexing the database

The search index is rebuilt every night (see Scheduler). You may also start it manually under Administration → System → Reindex, e.g. after restoring a backup.

4.3. Analysing tools

The log files are located in logs of the home directory; administrators may also search them with the log viewer of the System dashboard. For analysing a hanging or slow server, the JDK tools are useful:

1
2
3
jps -l                 # Lists the Java processes and their pids.
jstack <pid>           # Thread dump, e.g. for finding blocked threads.
jcmd <pid> GC.heap_info

In Docker, run them inside the container (docker exec -it projectforge bash); the JRE image may not contain all JDK tools, then kill -3 <pid> writes a thread dump to the container’s output (docker logs projectforge).

4.4. Backup

All datasets of ProjectForge® are stored in the underlying database (e. g. PostgreSQL). You should install a Unix cron script making frequently a backup of the database (see Backup script).

4.4.1. Embedded database (HSQLDB)

With the embedded database, everything is located in ProjectForge’s home directory: the database (database), the configuration files, your resources and the data transfer files (datatransfer). The files of the file store are part of the embedded database. For a backup, stop ProjectForge and copy the whole home directory (e.g. tar czf projectforge-home_2026-10-06.tar.gz -C $HOME ProjectForge); logs and hibernateSearch may be left out. For restoring, stop ProjectForge, replace the home directory by the copy and start ProjectForge again (rebuild the search index if you left out hibernateSearch). If the data transfer files are encrypted, the key mustn’t be part of this backup, see Encryption of the data transfer files.

4.4.2. PostgreSQL dumps

The files of the file store (attachments of contracts, orders, invoices etc.) are stored in the own schema pf_files. This schema may become large, so the files get a dump of their own. The data of the CVE-Clinic sources isn’t backed up:

  • The main dump without the files and without the data of the CVE-Clinic sources: pg_dump -N pf_files --exclude-table-data=…​ …​ (excludes the schema pf_files, the tables of the sources stay, empty). The data of the sources (the imported details of the vulnerabilities, the OSV advisories, their indexes for the matching and the state of the imports) is imported again from the sources (NVD, EUVD, OSV) after restoring, see Data base. The cases, team checks, answers, the inventory and the findings are part of the main dump, and so are the vulnerabilities themselves (id and CVE id), which they refer to.

  • The dump of the files only: pg_dump -n pf_files -Fc …​ (custom format, restorable in parallel by pg_restore -j).

The dump of the files is independent of the main dump: there are no foreign keys between pf_files and the other tables. Differences after restoring dumps of different days are reported by the sanity checks, which run nightly (projectforge.cron.sanityChecks) and on demand via Administration → System → Check system integrity.

Data transfer files (<home>/datatransfer) are not part of the dumps: their metadata is in pf_files, but the content is stored in the file system. The backup script archives them as projectforge_datatransfer_…​tar (uncompressed, the files are already stored compressed). If they are encrypted, the key isn’t part of this archive, see Encryption of the data transfer files.

4.5. Restoring a backup

4.5.1. Data base

1
2
gunzip projectforge_2026-10-06_02-00.sql.gz
psql -q -f projectforge_2026-10-06_02-00.sql -U postgres projectforge > log.txt 2>&1

The cases, team checks, answers and findings are complete, but the vulnerabilities have no details (description, CVSS, …​) and the OSV advisories are missing until the imports fill them again, by themselves: the OSV import reads all advisories again, the NVD import the vulnerabilities modified in the last initialDays and then the NVD since 1999 for the stored vulnerabilities without details (see Vulnerabilities from the NVD). Until an import filled a vulnerability, its findings stay as they are.

Restoring the files afterwards (schema pf_files, the main dump doesn’t contain it):

1
pg_restore -U postgres -d projectforge -j 4 projectforge_files_2026-10-06_02-00.dump

Restoring the data transfer files (from the same backup as the files dump, because their metadata is in pf_files), with ProjectForge stopped and after removing the existing directory datatransfer:

1
tar xf projectforge_datatransfer_2026-10-06_02-00.tar -C ~/ProjectForge

Restore all dumps before starting ProjectForge: the main dump contains the Flyway history, so Flyway doesn’t create the schema pf_files again. If the schema already exists in the target data base, drop it first (DROP SCHEMA pf_files CASCADE;) or use pg_restore --clean --if-exists. After restoring, run the sanity checks via Administration → System → Check system integrity (they also report missing data transfer files).

For dropping and creating a new database, see PostgreSQL. Don’t forget to rebuild the search index afterwards (Administration → System → Reindex)!

Hint

If you want to use productive data in a test system, change the users' e-mail addresses (e.g. to your own) in the test database first, so nobody gets notifications from the test system. Please also mind data protection.

4.5.2. Updating from an installation with JCR

Since 9.2 there is no JCR (Apache Oak) any more: all files are stored in the data base (schema pf_files), the data transfer files in the file system (<home>/datatransfer). ProjectForge 9.1.x is the last release supporting the JCR. If you still use the JCR, update to 9.1.x first, switch there to projectforge.files.store=db and wait for a successful migration (Result: OK in the report <home>/jcr-migration-report.txt), see the admin guide of 9.1.x. Releases without JCR can’t read the JCR, and they don’t import a JCR backup ZIP.

On start-up, ProjectForge refuses to start, if the former JCR (<home>/jcr, projectforge.dir.jcr, the jcrDirectory of a former config.xml or oak.datasource.url) contains data, which wasn’t migrated: the files would be missing otherwise. This is the case without a successful migration report, with projectforge.files.store=jcr (files uploaded after switching back to the JCR) and if the file store contains no file from before the migration (database restored to a state before the migration). If the JCR doesn’t contain any files to keep, the start may be forced by projectforge.files.ignoreLegacyJcr=true. After a successful start, the directory <home>/jcr (or the Oak tables of oak.datasource.url) isn’t used anymore and may be deleted after a backup. Remove projectforge.dir.jcr and oak.datasource.url from projectforge.properties together with the JCR, not before.

4.6. Automatical backup

4.6.1. Backup script

Install a crontab entry as user projectforge on your ProjectForge® server (or the database server) starting the following script:

File: pforgebackup.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/bin/bash

PGSQL_DIR=/usr/bin
BACKUPDIR=/home/projectforge/backup
# Home directory of ProjectForge:
PF_HOME=/home/projectforge/ProjectForge
BASENAME=projectforge
TIMESTAMP=`date '+%Y-%m-%d_%H-%M'`

echo Starting backup of ProjectForge ...

# Main dump: all data except the files (schema pf_files) and the data of the CVE-Clinic sources.
BACKUPFILE=$BACKUPDIR/${BASENAME}_${TIMESTAMP}.sql
# Dump of the files only (schema pf_files, attachments of contracts, orders, invoices etc.).
FILES_BACKUPFILE=$BACKUPDIR/${BASENAME}_files_${TIMESTAMP}.dump
# Archive of the data transfer files (content in the file system, metadata in pf_files).
DATATRANSFER_BACKUPFILE=$BACKUPDIR/${BASENAME}_datatransfer_${TIMESTAMP}.tar
# The data of the CVE-Clinic sources isn't backed up (the tables stay, empty): the imports fill them again after
# restoring. The vulnerabilities themselves (id and CVE id), which cases and findings refer to, are part of the main dump.
CVERADAR_SOURCES="t_plugin_cveradar_vulnerability_detail t_plugin_cveradar_cpe_match t_plugin_cveradar_osv_advisory
  t_plugin_cveradar_purl_match t_plugin_cveradar_import_state"

echo Using filenames $BACKUPFILE, $FILES_BACKUPFILE and $DATATRANSFER_BACKUPFILE ...

# Also without the CVE-Clinic plugin: --exclude-table-data doesn't fail on missing tables.
CVERADAR_EXCLUDE=
for TABLE in $CVERADAR_SOURCES; do
  CVERADAR_EXCLUDE="$CVERADAR_EXCLUDE --exclude-table-data=$TABLE"
done

echo Exporting data ...
$PGSQL_DIR/pg_dump -U projectforge -N pf_files $CVERADAR_EXCLUDE -f $BACKUPFILE || { echo "Error: export of data failed."; exit 1; }

echo Zipping backupfile ...
gzip $BACKUPFILE

echo Exporting files ...
# Custom format; compressed (-Z 1, with PostgreSQL 16+ also -Z zstd:1): pg_dump writes the file contents (bytea) hex
# encoded, which doubles their size, even if the files themselves are already compressed.
$PGSQL_DIR/pg_dump -U projectforge -n pf_files -Fc -Z 1 -f $FILES_BACKUPFILE || { echo "Error: export of files failed."; exit 1; }

if [ -d $PF_HOME/datatransfer ]; then
  echo Archiving data transfer files ...
  # Uncompressed: the files are already stored compressed (or encrypted). Exit code 1 of tar means that files changed
  # while reading (e.g. an upload during the backup), the archive is usable anyway.
  tar cf $DATATRANSFER_BACKUPFILE -C $PF_HOME datatransfer
  [ $? -le 1 ] || { echo "Error: archiving of data transfer files failed."; exit 1; }
fi

# For using scp for remote backup:
#scp $BACKUPFILE.gz $FILES_BACKUPFILE $DATATRANSFER_BACKUPFILE <user>@backup.mydomain:backup/

# Deleting of daily backups is done by ProjectForge (projectforge.cron.purgeBackupDir, see below). Monthly backups
# are kept forever, because they're needed for financial, yearly comparisons.

echo Backup of ProjectForge finished.

ProjectForge purges the daily backups (keeping weekly and monthly ones), if the backup dir is configured in projectforge.properties:

projectforge.cron.purgeBackupDir=/home/projectforge/backup
# Optional: comma-separated prefixes of the backup series. If not given, all files containing a date are processed.
# Each series (text in front of the date, here projectforge_, projectforge_files_ and projectforge_datatransfer_) is
# purged on its own.
#projectforge.cron.purgeBackupFilesPrefix=projectforge_

The files dump projectforge_files_…​ and the archive projectforge_datatransfer_…​ are series of their own, so they are purged like the main dump (also with the prefix projectforge_, because they start with it).

For allowing pg_dump without prompting a password, run the script as the Unix user projectforge and add the following line to pg_hba.conf (peer authentication: the Unix user name must match the database user name):

File: pg_hba.conf
1
local projectforge projectforge peer

Alternatively, store the password in ~/.pgpass of the user running the script (chmod 600 ~/.pgpass): localhost:5432:projectforge:projectforge:<password> (then add -h localhost to the pg_dump and psql calls).

4.6.2. Adding crontab

Adding following entry to the Unix crontab:

1
2
# run every day at 2:00 am
0 2 * * * /home/projectforge/bin/pforgebackup.sh

4.6.3. Remote backup

For remote backup via scp you need to use key authorization:

  1. Generate keys (using empty password) ssh-keygen

  2. Copy your public key, e. g. identity.pub to your backup server.

  3. Add this key on the backup server to the authorized keys.

4.6.4. Polling remote backup

For security reasons it’s also possible to get the backup files from the ProjectForge® server with a cronjob starting e. g. one hour later:

File: pforgePollBackup.sh
#!/bin/bash
SERVER=projectforge.mydomain
DIR=/home/projectforge/backup
REMOTE_DIR=backup REMOTE_USER=projectforge
# Add e.g. -e "ssh -p <port>" if ssh doesn't use the default port.
rsync -avz -C $REMOTE_USER@$SERVER:$REMOTE_DIR $DIR

In that case you have to generate ssh key on your backup server and to add it to the authorized keys on your ProjectForge® server. Add the following entry to your crontab on the backup server:

1
2
# run every day at 3:00 am
0 3 * * * /home/projectforge/bin/pforgePollBackup.sh

4.7. Monitoring

You may monitor ProjectForge with your monitoring tool (such as Nagios) by calling the public system status, e.g. projectforge.acme.com/rsPublic/systemStatus. It answers with a small JSON object as soon as the web server is up. Without a login, version and build date are only placeholders (<version>, <date>), so only the application name (appname) and the start-up state are useful for monitoring: setupRedirectUrl is given as long as the setup isn’t completed.

Problems at runtime (errors, unreachable external systems, failed scheduler runs) are reported by the Support error digest and shown on the System dashboard.

4.8. Scheduler

4.8.1. Data base search index

The search index is rebuilt every night by the nightly job (projectforge.cron.nightly, default 2:30). ProjectForge runs with the time zone UTC (independent of the server’s time zone), so the cron expressions of projectforge.cron.* are UTC. Errors are logged and therefore part of the error digest. The runs of all scheduled jobs are listed on the System dashboard.

4.9. Uninstalling ProjectForge

Delete the JAR file (or the Docker container and image) and ProjectForge’s home directory. If you’re not sure where the home directory is located, look at the log output of the start-up: ProjectForge logs the home directory it uses. If you use PostgreSQL, drop the database as well (see PostgreSQL).

Hint

Please note: If you delete the home directory, all data of the embedded database is deleted as well!

4.10. Security improvements

4.10.1. Fail2ban

Failed logins are logged as warnings, including the client’s IP address (<user>@<ip> in front of the logger name):

…​ WARN @192.0.2.10 …​ LoginDefaultHandler : User login failed: jdoe/**

A starting point for a fail2ban filter (please verify it with fail2ban-regex against your log file logs/ProjectForge.log):

[Definition]
failregex = @<HOST>\s.*LoginDefaultHandler\s*: User login failed:

5. Plugins

Some features are built-in plugins which aren’t enabled automatically. They are activated by an administrator under Administration → Plugins (or via projectforge.plugins.ensure-active in projectforge.properties). Please restart ProjectForge after (de-)activating a plugin, see also Installation – Activation of built-in plugins.

5.1. Data transfer

Data transfer areas for exchanging (large) files between users, groups and also external users. Recommended for most installations. The files are stored in datatransfer of the home directory and expire automatically.

5.1.1. Encryption of the data transfer files

The data transfer files are stored compressed (unless already compressed by their format, e.g. zip, pdf, videos) and, if a key is configured, encrypted (AES-256). So copies of the home directory (e.g. backups) don’t contain them in plain text:

# A long random value, e.g. generated by: openssl rand -base64 32
projectforge.files.fileSystemEncryptionKey=...
  • Keep the key apart from the backups of the home directory, otherwise they contain the key too. Instead of projectforge.properties, the key may be given as environment variable PROJECTFORGE_FILES_FILESYSTEMENCRYPTIONKEY.

  • Keep a copy of the key in a safe place: if it’s lost or changed, the encrypted files can’t be read anymore (the sanity check reports them). A missing or changed key is also logged as error on start ("Encrypted files below … can’t be read").

  • Only files stored after configuring the key are encrypted, files stored before keep their format and stay readable.

  • The attachments stored in the data base (schema pf_files) aren’t encrypted, only the data transfer files.

  • The costs are low: encryption runs with several GB/s per core (Java 21), compressing a text file of 10 GB takes about a minute.

5.2. Liquidity planning

Supports the cash flow forecast for an easy-to-use liquidity planning. Please refer to the User guide for further information.

5.3. Banking

Bank accounts and the import of bank account statements.

5.4. Software license management

In most companies you’ve to deal with lots of software and license keys. This plugin allows administrators and users to organize license keys:

  • Every user is able to add a software entry with license keys.

  • The administrator is able to view and edit all license keys.

  • Only users who are assigned to a software entry (owners) are able to see the license key.

  • The history of every entry allows to track license usage over time by the users.

  • The history of changes is only visible for administrators and software owners.

5.5. Merlin Word® templates

Templating for Microsoft Word® documents (with variables, dependent variables as well as serial execution), useful for contracts, serial documents etc.

5.6. Skill matrix

Capture and represent the skills of the employees, managed by the users themselves.

5.7. CVE-Clinic

Vulnerability management for the security team: cases (vulnerabilities with CVE id and incidents without one), team checks of projects and systems, the confirmations of the employees and a closure overview, which shows whether a case can be closed. As evidence on the client devices, a central log server (Graylog) with the messages of a client agent can be connected; it is optional.

The plugin is under development. The page Miscellaneous → CVE-Clinic lists the vulnerabilities imported from the NVD (see Vulnerabilities from the NVD); the pages Cases, Devices and My CVE-Clinic work on real data as well. The remaining functions can be tried as a prototype with fictitious demo data, linked prominently on the start page.

5.7.1. Activation

Activate the plugin cveradar under Administration → Plugins (or with projectforge.plugins.ensure-active=cveradar in projectforge.properties) and restart ProjectForge, see Plugins.

5.7.2. Rights and roles

The roles are PF groups; their names are configured (see General settings), the groups themselves are created and maintained like any other group under Administration → Groups. Each role includes the weaker ones, a group that isn’t configured or doesn’t exist gives nobody the role, and the PF admin role alone grants no access to the CVE-Clinic.

Role Property Who and what

Readers

readGroup, e.g. cve-read

E.g. the management: the vulnerabilities from the NVD, cases with their ratings and measures, the closure overviews (counts only, no persons, no devices). They take part in the discussion of a case.

Security team

writeGroup, e.g. cve-read-write

Opens, takes over and assigns cases, rates, sets the status and the measures, requests team checks, sees the devices with their persons, moderates the discussions.

Security officers

securityOfficerGroup

Additionally set the binding priority of cases of others, close a case with open items (with a reason) and approve the risk acceptance of a finding.

Management

managementGroup

Reads like the readers and approves the risk acceptance of a finding from priority P2 on besides the security officer (two persons).

everybody

On My CVE-Clinic the own devices, the cases released to the clients (to confirm the measures) and the team checks of the own groups.

  • The binding priority of a case is set, with a reason, by its assignee, or by a security officer. While nobody is assigned, any member of the security team may; they become the assignee.

  • A team check is answered by the members of the requested group, no CVE-Clinic group is needed for it. The file of a team check, with its own discussion, is open to this group, the security team and the security officers only.

  • A team check may ask every member (e.g. everyone checks the own client): each one answers for themselves, and the check stays open until all members answered who logged in within teamCheckActiveDays (90 by default). The security team sees all answers and who is still missing.

  • The discussion of a case is open to all readers. Project and system details belong into the file of a team check, not there.

5.7.3. General settings

All settings are in projectforge.properties (or in the properties editor, see Properties editor) and take effect after a restart:

# Ids of incidents without CVE: INC-2026-1, INC-2026-2, ... (letters and digits only).
projectforge.plugin.cveradar.caseIdPrefix=INC
# A device not reported for more days is inactive and not counted in the closure overview.
projectforge.plugin.cveradar.clientActiveDays=30
# Devices not reported for more days are deleted with their scan findings (personal data).
projectforge.plugin.cveradar.deviceRetentionDays=365
# A team check asking every member waits for the members logged in within so many days only.
projectforge.plugin.cveradar.teamCheckActiveDays=90
# PF groups of the roles (see above).
projectforge.plugin.cveradar.readGroup=cve-read
projectforge.plugin.cveradar.writeGroup=cve-read-write
projectforge.plugin.cveradar.securityOfficerGroup=security-officers
# Bulk import of SBOMs into an area: largest zip in MB, unless the security officer sets another size for the area,
# and the most the security officer may set. A reverse proxy needs to let a request of this size through
# (nginx: client_max_body_size).
projectforge.plugin.cveradar.sbomImport.maxSizeMb=20
projectforge.plugin.cveradar.sbomImport.maxSizeLimitMb=1024
projectforge.plugin.cveradar.managementGroup=management

The zip of a bulk import of SBOMs (see the user guide) is stored in the temporary directory of the JVM while it is read and deleted afterwards; it needs that much free space there.

The outdated devices are deleted by the job of the connector (see Jobs and connections).

5.7.4. Log server (Graylog)

The client agent (e.g. of the client management tool) logs its version and, optionally, the results of its checks to a central log server. ProjectForge searches these messages with the search API of Graylog and keeps the newest version and the newest result of every check per device.

projectforge.plugin.cveradar.graylog.url=https://graylog.example.org
# Access token of a Graylog user with read access to the stream.
projectforge.plugin.cveradar.graylog.token=...
# Stream with the messages of the client agents; empty: all streams the user may read.
projectforge.plugin.cveradar.graylog.streamId=...
# Query selecting the messages and the period searched, back from now.
projectforge.plugin.cveradar.graylog.query=source:client-agent
projectforge.plugin.cveradar.graylog.rangeSeconds=86400
projectforge.plugin.cveradar.graylog.limit=10000
# Field names of the messages.
projectforge.plugin.cveradar.graylog.userField=agent_user
projectforge.plugin.cveradar.graylog.serialField=agent_serial
projectforge.plugin.cveradar.graylog.hostField=agent_host
projectforge.plugin.cveradar.graylog.versionField=agent_version
projectforge.plugin.cveradar.graylog.checkIdField=agent_check_id
projectforge.plugin.cveradar.graylog.checkResultField=agent_check_result
# Hourly at minute 15.
projectforge.plugin.cveradar.graylog.cron=0 15 * * * *
  • Mandatory are url, token, query, userField and at least one of versionField and checkIdField; otherwise the connector is inactive.

  • Devices are matched by serial number; without serial field by user name and host name.

  • A check result passed, true or ok counts as passed, anything else as failed.

5.7.5. Vulnerabilities from the NVD

ProjectForge imports the vulnerabilities (CVEs) from the National Vulnerability Database (NVD API 2.0) and lists them on the start page Miscellaneous → CVE-Clinic. The import is off by default, as it needs internet access.

projectforge.plugin.cveradar.nvd.enabled=true
# Optional: an API key raises the rate limit from 5 to 50 requests in 30 seconds
# (https://nvd.nist.gov/developers/request-an-api-key).
projectforge.plugin.cveradar.nvd.apiKey=...
# The first run imports the vulnerabilities modified in the last 30 days (no back-fill of history).
projectforge.plugin.cveradar.nvd.initialDays=30
# New vulnerabilities from this CVSS score on.
projectforge.plugin.cveradar.nvd.minCvss=7.0
# Every 2 hours at minute 17 (UTC).
projectforge.plugin.cveradar.nvd.cron=0 17 */2 * * *
  • The first run imports the vulnerabilities modified in the last initialDays, every run after that the ones modified since the last complete run (in pieces of at most 120 days, as the NVD allows). A run aborted by an error continues there.

  • After restoring the main dump of a backup (without the data of the sources, see Data base), the first run finds stored vulnerabilities without details: after the last initialDays, it reads the NVD since 1999 once more and fills them, without adding new ones (about an hour, quicker with an API key; an aborted run continues there).

  • The score is the one of the NVD; as long as the NVD hasn’t rated a vulnerability, the score of the assigning authority (CNA) counts. A vulnerability without any score is imported as soon as it is rated.

  • A stored vulnerability is always updated, also when the NVD rejects it (shown struck through) or rates it lower.

  • Reading the list needs a role of the CVE-Clinic (see Rights and roles). A row opens the file of the vulnerability.

  • The API key is sent as header only and is never logged or shown by the properties editor.

5.7.6. Advisories of the libraries from OSV.dev

The NVD often names libraries late or under another name than the package (or not at all for weeks). Therefore ProjectForge also imports the advisories of OSV.dev (GitHub, PyPA, Go, RustSec, …​) for the ecosystems of the libraries and matches them by package (purl) and version against the libraries of the inventory, i.e. the ones from SBOMs, lockfiles and build lists. The import is off by default, as it needs internet access.

projectforge.plugin.cveradar.osv.enabled=true
# The public bucket of OSV.dev.
projectforge.plugin.cveradar.osv.url=https://osv-vulnerabilities.storage.googleapis.com
# The ecosystems and distributions (OSV keys), comma separated.
projectforge.plugin.cveradar.osv.ecosystems=Maven,npm,PyPI,NuGet,Packagist,Go,crates.io,Debian,Ubuntu
# Also the malicious packages (MAL-...): some hundred thousand advisories, almost all of npm and PyPI.
projectforge.plugin.cveradar.osv.malicious=false
# Every 6 hours at minute 47 (UTC).
projectforge.plugin.cveradar.osv.cron=0 47 */6 * * *
  • The first run reads all advisories of each ecosystem (the file all.zip of the bucket, npm about 250 MB), every run after that the ones modified since. Only files of advisories are fetched; nothing of the inventory leaves ProjectForge.

  • An advisory becomes a vulnerability of the list only when it matches a library of the inventory, then whatever its score: the threshold of the NVD import (minCvss) holds for products, not for libraries in use. It holds for a match that is only possible (a library without version, a jar listing without groupId), else every advisory of an artifact name would become a vulnerability. The vulnerability is named by its CVE id, without one by its GHSA id (or the id of the source); the advisories of one CVE (e.g. of GitHub and of PyPA) become one vulnerability.

  • As soon as the NVD modifies the CVE, its data replace the ones of the advisories (status OSV before); the advisories keep matching.

  • A library without groupId (a jar listing) matches by its artifact only, as possible. Where the advisories know the package of a library, they decide: a possible match of the same vulnerability by the name of the CPE is dropped.

  • A withdrawn advisory matches nothing any more; a vulnerability made of withdrawn advisories only is rejected.

  • The ad-hoc check uses the advisories as well, without storing anything.

  • Distributions (Debian, Ubuntu): the packages of a container or server (SBOM with the distro qualifier in the purl, e.g. pkg:deb/debian/curl@7.88.1-10?distro=debian-12, or a dpkg-query list with the lines of /etc/os-release, see the user guide) are matched against the advisories of their release, the versions compared as dpkg does. The CPE of the upstream project doesn’t know the fixes a distribution backports (curl 7.81.0-1ubuntu1.30 is fixed, though below 7.84.0); where the distribution knows the package for a CVE in that release, it decides, and a possible match by the CPE is dropped. A CVE the distribution rates without impact (Debian unimportant, Ubuntu negligible) matches nothing. A vulnerability made of advisories of a distribution only needs the threshold of the NVD import (minCvss). Only the advisories per CVE are imported (DEBIAN-CVE-…, UBUNTU-CVE-…), not the notices (DSA, DLA, USN), and without the kernels (linux, linux-aws, …), which an inventory names by their binary packages; their documents aren’t kept. Ubuntu’s all.zip is about 800 MB, Debian’s 80 MB. The advisories name the source package: a binary package is matched by its source (the upstream qualifier of the purl, libssl3 → openssl, with its version if that differs). A package list without release (dpkg-query without /etc/os-release) keeps matching by the CPE only.

  • Against false positives of a match by the name of a CPE: an entry for a runtime (target_sw node.js, rust, python, …) only meets libraries of that ecosystem (isaacs:tar:…:node.js isn’t the Debian package tar). For the libraries of an ecosystem imported from OSV, a match by name needs a vendor that fits the package, its scope or groupId (adobe:connect isn’t the npm package connect, fasterxml:jackson-databind is com.fasterxml.jackson.core:jackson-databind).

5.7.7. Vulnerabilities from the EUVD

The European Union Vulnerability Database (EUVD) of the ENISA supplements the NVD. ProjectForge reads its public API (no key needed) and adds the EUVD id, the EPSS (probability of exploitation) and the day since the EUVD knows a vulnerability to be exploited. It also imports vulnerabilities the NVD doesn’t have (yet), among them ones without a CVE id. The import is off by default, as it needs internet access.

projectforge.plugin.cveradar.euvd.enabled=true
projectforge.plugin.cveradar.euvd.url=https://euvdservices.enisa.europa.eu/api
# The first run imports the vulnerabilities published in the last 30 days (no back-fill of history).
projectforge.plugin.cveradar.euvd.initialDays=30
# Every run reads the publications of the last 7 days again (rating and CVE id often come later).
projectforge.plugin.cveradar.euvd.rescanDays=7
# New vulnerabilities from this CVSS score on.
projectforge.plugin.cveradar.euvd.minCvss=7.0
# Pause between two requests in milliseconds.
projectforge.plugin.cveradar.euvd.pauseMillis=1000
# Every 2 hours at minute 37 (UTC), after the NVD import.
projectforge.plugin.cveradar.euvd.cron=0 37 */2 * * *
  • The EUVD filters by the day of publication, not by the day of the last change. That is why every run reads the last rescanDays days again, and why a change of an older entry reaches ProjectForge only through the daily read of all entries known to be exploited (it updates the vulnerabilities stored, it doesn’t add new ones).

  • A vulnerability of the NVD (or of OSV.dev) gets the EUVD id, the EPSS and the day of exploitation, nothing else; only a vulnerability without any rating also gets the CVSS score of the EUVD. The NVD stays the source of the details.

  • A vulnerability the NVD doesn’t have is named by its CVE id, without one by its EUVD id (status EUVD). Its details come from the EUVD until the NVD modifies the CVE and takes it over. New ones are imported from minCvss on; an entry the EUVD hasn’t rated is skipped until it is.

  • The EUVD has no CPE and no version ranges, only the names of vendor and product. A library or product whose name equals such a product name is therefore matched as possible, never as affected. As soon as the NVD gives CPE entries for the CVE, these replace the entries made of the names.

  • The EUVD doesn’t document a rate limit; ProjectForge pauses between requests and retries with a growing delay.

5.7.8. Dependency-Track (optional)

Dependency-Track can detect the findings in parallel to the own matching. The connection is optional: without url and API key ProjectForge works as without it. Dependency-Track runs as a service of its own (API server, frontend, PostgreSQL), operated by the administrators; its frontend isn’t needed by the users. So far ProjectForge only tests the connection; the transfer of the inventory and of the findings follows.

projectforge.plugin.cveradar.deptrack.url=https://dtrack.example.org
# Better given as environment variable PROJECTFORGE_PLUGIN_CVERADAR_DEPTRACK_APIKEY.
projectforge.plugin.cveradar.deptrack.apiKey=...
# Test of the connection, every 15 minutes.
projectforge.plugin.cveradar.deptrack.probeCron=0 */15 * * * *
  • In Dependency-Track, create a team for ProjectForge (Administration → Access Management → Teams) with the permissions BOM_UPLOAD, PORTFOLIO_MANAGEMENT, PROJECT_CREATION_UPLOAD, VIEW_POLICY_VIOLATION, VIEW_PORTFOLIO, VIEW_VULNERABILITY and VULNERABILITY_ANALYSIS, and generate its API key.

  • Leave the external analysers (OSS Index, Snyk, VulnDB) off, they would send the list of components outside; enable the vulnerability sources NVD, GitHub Advisories and OSV. Switch off the task Telemetry Submission.

  • The tile Dependency-Track (CVE-Clinic) of the system dashboard shows the host, the version and the result of the last test: down if Dependency-Track isn’t reachable or refuses the key, degraded if the team misses a permission (listed in the log and in the run of the job). A failing Dependency-Track doesn’t affect the other pages.

  • Use an https url: the API key is sent with every request (a plain http url other than localhost is logged as warning). Redirects aren’t followed, so give the url of the API server itself.

5.7.9. Jobs and connections

  • The connector and the imports run as jobs, CVE-Clinic: client agents, CVE-Clinic: NVD import, CVE-Clinic: OSV import, CVE-Clinic: EUVD import and CVE-Clinic: Dependency-Track probe, listed under Administration → Job monitor. There they can be started at once (Run now); while their source isn’t configured, they are shown as inactive with the reason. The summary of every run is shown in the run and logged, errors are part of the error digest.

  • Timeouts and the max size of a response are set per connection with projectforge.integration.clients.cveradar-graylog. and projectforge.integration.clients.cveradar-nvd., projectforge.integration.clients.cveradar-osv., projectforge.integration.clients.cveradar-euvd., projectforge.integration.clients.cveradar-deptrack.* (e.g. responseTimeoutMs, maxResponseBytes, default 50 MB; for the NVD 180 s and 200 MB, as a page has up to 2000 vulnerabilities; for OSV.dev 180 s and 1 GB for the zip of an ecosystem, downloaded into a temporary file).

  • An outgoing proxy isn’t supported yet: ProjectForge must reach the hosts directly.

  • Keys and the token are never shown by the properties editor. Instead of projectforge.properties they may be given as environment variables, e.g. PROJECTFORGE_PLUGIN_CVERADAR_GRAYLOG_TOKEN.

5.7.10. The file of a CVE

A row of the list of vulnerabilities opens the file of the CVE: the NVD data (the remediation hints of the references apart from the others), the ratings, the measures, the discussion and the team checks.

  • Opening: At first the file is the NVD page only. The first click on Open file (or on Start the discussion) creates the case of the CVE; all readers may. The NVD import changes nothing of the case.

  • Rating: Every member of the security team gives a rating of their own (priority, exploitability here, reason); they stand side by side, deviations are marked. The suggested priority comes from the CVSS score, a listing in the CISA KEV catalogue raises it by one step. The binding priority is set by the assignee or a security officer, with a reason.

  • Measures: Fix version, workaround, own references (http and https links only) and the measures for techies and non-techies as formatted text.

  • Editing at the same time: If someone changed the case meanwhile, the server refuses the change; the file is reloaded and the change repeated.

  • Discussion: The chat of the file is open to all readers. Team checks have a file of their own with a discussion of its own, which only the asked group, the security team and the security officers see.

5.7.11. A first case without connectors

A case can be worked through without any connector:

  1. Cases → New case: an incident (the id is given automatically) or a CVE, with the measures for techies and non-techies. Released to employees shows it to every employee on My CVE-Clinic.

  2. In the closure overview of the case: Request team check for every project or system, with the group that checks it.

  3. The groups answer their checks, the employees confirm the measures on My CVE-Clinic.

  4. As soon as nothing is open, the closure overview shows Can be closed; the security team closes the case with a reason (Close case).

With the connectors running, the fix criteria of the case are the evidence on the devices: a minimum version of the client agent and the id of a check of the agent.

5.8. Further plugins

A few more plugins exist for compatibility with existing installations. They are listed under Administration → Plugins as well, but we don’t develop them further.