Administration guide
Hints, tips and best practices for the administration of ProjectForge
- 1. Introduction
- 2. ProjectForge Setup
- 3. Configuration
- 3.1. Base directory (application’s home)
- 3.2. Configuration file application.properties
- 3.3. Switching off functions
- 3.4. Mobile app
- 3.5. Customization
- 3.6. Configuration settings (web interface)
- 3.7. Using ttf fonts for PDF render engine
- 3.8. HTML formatted E-Mail templates
- 3.9. Sending e-mails
- 3.10. Support error digest
- 3.11. System dashboard and log viewer
- 3.12. Phone calls and texting with ProjectForge
- 3.13. Digital business cards
- 3.14. Number ranges of customers, projects and cost units
- 3.15. Group nomenclature
- 3.16. business assessment / BWA
- 3.17. SecurityConfig
- 3.18. LDAP
- 3.19. OAuth2 / OIDC Authentication
- 3.20. Identity Provider Sync (IdP)
- 3.21. Troubleshooting
- 4. Maintenance
- 5. Plugins
- 5.1. Data transfer
- 5.2. Liquidity planning
- 5.3. Banking
- 5.4. Software license management
- 5.5. Merlin Word® templates
- 5.6. Skill matrix
- 5.7. CVE-Clinic
- 5.7.1. Activation
- 5.7.2. Rights and roles
- 5.7.3. General settings
- 5.7.4. Log server (Graylog)
- 5.7.5. Vulnerabilities from the NVD
- 5.7.6. Advisories of the libraries from OSV.dev
- 5.7.7. Vulnerabilities from the EUVD
- 5.7.8. Dependency-Track (optional)
- 5.7.9. Jobs and connections
- 5.7.10. The file of a CVE
- 5.7.11. A first case without connectors
- 5.8. Further plugins
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
-
Extract:
keytool -exportcert -alias selfsigned -keystore keystore.p12 -storetype PKCS12 -file selfsigned.crt -rfc -
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.
2.3.6. Previews of links in the chat
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.
-
Create the user (e.g. as admin user):
createuser -U postgres -P projectforge -
Create the database:
createdb -U postgres -E UTF-8 -O projectforge projectforge -
You can drop an existing user and database with:
dropdb projectforgeanddropuser projectforge -
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:
|
Configuration file, written by the setup wizard. A former |
|
The embedded database (HSQLDB), if used. |
|
The search indices (Lucene). If corrupted, stop ProjectForge, delete the content of this directory and rebuild the indices via Administration → System → Reindex. |
|
The log files, e.g. |
|
Your own resources, overriding the built-in ones: |
|
Default directory for backups. |
|
The files of the data transfer areas (plugin Data transfer). |
|
The former document repository (JCR), not used anymore since 9.2. May be deleted, see Updating from an installation with JCR. |
|
Working directory, e.g. for states of synchronizations. |
|
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:
-
Create one or more properties files in the
~/ProjectForge/resourcesdirectory:-
CustomerI18nResources.properties- for default/English translations -
CustomerI18nResources_de.properties- for German translations -
CustomerI18nResources_en.properties- for English translations (explicit)
-
-
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
-
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.propertiesfiles 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.
projectforge.propertiesprojectforge.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=
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.*
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:
-
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. -
In this Pass Type ID: Create Certificate with a certificate signing request, download
pass.cer. -
Import
pass.cerinto the keychain (next to the private key of the request) and export both aspass.p12. -
Download the intermediate certificate Apple Worldwide Developer Relations - G4 (apple.com/certificateauthority).
-
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 |
|---|---|
|
At least one range is configured. Only the configured ranges apply. |
|
No range is configured, but Kost2 of projects in the formerly built-in ranges exist: 4 for internal
projects ( |
|
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 |
|---|---|---|
|
First digit (1-9) of the project and Kost2 numbers of this range, unique for all ranges. |
|
|
Display name, e.g. |
|
|
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 |
|
|
2 |
Number of digits of the project number per customer (1-5), or of the whole range for a range without customers. |
|
|
Segments of the 8-digit Kost2 for display and input, e.g. |
Global settings:
| Property | Default | Description |
|---|---|---|
|
|
If |
|
|
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.
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
FREEcan’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.for1.#..). Withprojectforge.invoicing-quota.billed-by-invoiced-kost2-types=truethe 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:
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/usermatches. -
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_ACCOUNTandPASSWORD, 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.
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:
projectforge.properties1
2
3
...
projectforge.login.handlerClass=LdapMasterLoginHandler
...
-
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.
-
Any change of the LDAP server will be ignored and may be overwritten by ProjectForge.
-
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". -
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. -
Deleted Users
Deleted users will not be synchronized and removed in LDAP if exist. -
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). -
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. -
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. -
Groups and assigned users
Users will be added asuniqueMemberentries 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.
Figure 3: |
ProjectForge uses the fields and object classes shown in
this figure for users. The object class |
Figure 4: |
ProjectForge uses the fields and object classes shown in
this figure for groups. The field |
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:
Figure 5: |
ProjectForge is able to manage POSIX accounts in LDAP master mode. |
-
After clicking the Button
createbeside the UID number field ProjectForge will prefill all POSIX account fields:-
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.
-
The GID number will be preset with
projectforge.ldap.posixAccountsDefaultGidNumberconfigured inprojectforge.properties. -
The home directory will be the prefix
/home/followed by the user-name: e. g./home/jdoe(not configurable). -
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-classposixAccountonly if the POSIX account data were created in ProjectForge for this user.
3.18.6. Managing Samba accounts
projectforge.properties1
2
3
4
...
projectforge.ldap.sambaAccountsSIDPrefix=
projectforge.ldap.sambaAccountsPrimaryGroupSID=
...
Figure 6: |
ProjectForge is able to manage POSIX and Samba accounts in LDAP master mode. |
-
After clicking the Button
createbeside the UID number field ProjectForge will prefill all SAMBA account fields:-
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.
-
The GID number will be preset with the defaultSambaPrimaryGroupSID configured in
projectforge.properties. -
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-classsambaSamAccountonly 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:
projectforge.properties1
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).
projectforge.properties1
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:
-
Match the OIDC
subclaim against theidpExternalIdfield of ProjectForge users -
If no match is found, fall back to matching the
preferred_usernameclaim against the ProjectForge username -
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 |
|---|---|
|
Enable periodic sync push to the gateway instance |
|
Base URL of the gateway’s sync API endpoint |
|
Shared secret (must match |
|
Interval between sync pushes in milliseconds (default: 900000 = 15 minutes) |
|
Delay of the first sync push after start-up in milliseconds (default: 120000 = 2 minutes) |
|
URL of the gateway for browsers (external links of data transfer areas), only if it differs from |
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
-
Create a realm (e.g.
acme.com) -
Enable locales globally:
de,en -
Create a client
projectforge(confidential, service account enabled) -
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 |
|---|---|---|---|
|
0 |
100 |
|
|
0 |
255 |
|
|
Options: |
||
|
0 |
— |
Validated automatically ( |
|
0 |
255 |
|
|
0 |
255 |
|
|
0 |
255 |
|
|
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
-
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) -
Create a sync user in Admin with this role
-
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 |
|---|---|
|
No external IdP (default) |
|
ProjectForge is master; users/groups are pushed PF → IdP |
|
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 schemapf_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 bypg_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:
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):
pg_hba.conf1
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:
-
Generate keys (using empty password) ssh-keygen
-
Copy your public key, e. g.
identity.pubto your backup server. -
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:
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 variablePROJECTFORGE_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 |
|
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 |
|
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 |
|
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 |
|
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,
userFieldand at least one ofversionFieldandcheckIdField; otherwise the connector is inactive. -
Devices are matched by serial number; without serial field by user name and host name.
-
A check result
passed,trueorokcounts 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.zipof 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 thedistroqualifier in the purl, e.g.pkg:deb/debian/curl@7.88.1-10?distro=debian-12, or adpkg-querylist 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.30is fixed, though below7.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 (Debianunimportant, Ubuntunegligible) 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’sall.zipis about 800 MB, Debian’s 80 MB. The advisories name the source package: a binary package is matched by its source (theupstreamqualifier of the purl,libssl3→openssl, with its version if that differs). A package list without release (dpkg-querywithout/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_swnode.js,rust,python, …) only meets libraries of that ecosystem (isaacs:tar:…:node.jsisn’t the Debian packagetar). 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:connectisn’t the npm packageconnect,fasterxml:jackson-databindiscom.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
rescanDaysdays 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
minCvsson; 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_VULNERABILITYandVULNERABILITY_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
httpsurl: the API key is sent with every request (a plainhttpurl 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.andprojectforge.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.propertiesthey 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:
-
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.
-
In the closure overview of the case: Request team check for every project or system, with the group that checks it.
-
The groups answer their checks, the employees confirm the measures on My CVE-Clinic.
-
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.