keepassxc-browser icon indicating copy to clipboard operation
keepassxc-browser copied to clipboard

Incompatibility with Coolify

Open M3t0r opened this issue 5 months ago • 1 comments

Have you searched for an existing issue?

  • [x] Yes, I tried searching and reviewed the pinned issues

Brief Summary

Coolify is a software to deploy and manage Docker containers on one or more machines. It has user accounts that can be secured with TOTP. It requires re-entering your password for certain destructive actions (e.g. deleting a deployed container). It has many forms where some fields are detected as TOTP or other login fields by this browser extension.

For me specifically the TOTP detection is a problem because I've set the extension to auto-fill those and I lock my database after some inactivity. So when navigating to a page with a falsely identified TOTP field KeePassXC plops up and asks me for my password to unlock, even though it's not required.

Disabling all extension features on this site also prohibit me to fill in the password in confirmation dialogs of destructive actions via the righ-click context menu, so I have to open KeePassXC manually, search for the correct entry, and copy-paste the password myself.

I've already filed a bug earlier about TOTP fields in Zitadel, but the solution suggested there (adding a site-specific exemption) isn't practical because of the broken password prompts and the amount of different URL I'd have to specify to disable pages with wrong TOTP field detection and enable pages with password confirmation prompts.

Expected Versus Actual Behavior

Without site specific configuration: no wrong TOTP field detection (and automatic opening and password prompt of the database)

With "disable all features": right clicking into a password prompt and selecting "Fill Password Only" from the KeePassXC context menu will still fill in the password

Steps to Reproduce

Setup

Either in a small throw away VM (1 core, 1GB memory; Hetzner, DigitalOcean, VirtualBox, qemu, ...) install Coolify and register a account, or get a paid account in the Coolify Cloud.

Wrong detection of TOTP fields

  1. Login to your coolify instance
  2. Lock your KeePassXC database
  3. Enable "Automatically fill in single TOTP entries" in the browser extension
  4. Navigate to Projects
  5. Create a new project
  6. Click on "production" to enter the production environment
  7. Create a "Application" (e.g. "Docker Image" and fill in nginx:latest), it doesn't automatically start anything
  8. In the configuration of the app navigate to "Healthcheck"
  9. The browser extension thinks the HTTP Status Code field is a TOTP field and opens KeePassXC to unlock the database
HTML snippet and screenshot of Healthcheck form Image
<form wire:snapshot="{&quot;data&quot;:{&quot;resource&quot;:[{&quot;health_check_enabled&quot;:false,&quot;health_check_path&quot;:&quot;\/&quot;,&quot;health_check_port&quot;:null,&quot;health_check_host&quot;:&quot;localhost&quot;,&quot;health_check_method&quot;:&quot;GET&quot;,&quot;health_check_return_code&quot;:200,&quot;health_check_scheme&quot;:&quot;http&quot;,&quot;health_check_response_text&quot;:null,&quot;health_check_interval&quot;:5,&quot;health_check_timeout&quot;:5,&quot;health_check_retries&quot;:10,&quot;health_check_start_period&quot;:5,&quot;custom_healthcheck_found&quot;:false},{&quot;key&quot;:3,&quot;class&quot;:&quot;App\\Models\\Application&quot;,&quot;relations&quot;:[&quot;destination&quot;,&quot;destination.server&quot;,&quot;destination.server.settings&quot;,&quot;source&quot;,&quot;environment&quot;,&quot;environment.project&quot;,&quot;additional_servers&quot;,&quot;settings&quot;,&quot;previews&quot;],&quot;s&quot;:&quot;elmdl&quot;}]},&quot;memo&quot;:{&quot;id&quot;:&quot;o8C97v4RU8etMmr4ounI&quot;,&quot;name&quot;:&quot;project.shared.health-checks&quot;,&quot;path&quot;:&quot;project\/wgc8s4ckkocco04ckgssokc0\/environment\/mwogccoo0cwgk40wcooooco0\/application\/fkk044k4goss0wsk4k0csk44\/healthcheck&quot;,&quot;method&quot;:&quot;GET&quot;,&quot;children&quot;:[],&quot;scripts&quot;:[],&quot;assets&quot;:[],&quot;errors&quot;:[],&quot;locale&quot;:&quot;en&quot;},&quot;checksum&quot;:&quot;6be6c7eb3d672a6f9bd501b8f8ca938ad01768d92b72c0dc2dd1491c6d9d8a97&quot;}" wire:effects="[]" wire:id="o8C97v4RU8etMmr4ounI" wire:submit="submit" class="flex flex-col">
    <div class="flex items-center gap-2">
        <h2>Healthchecks</h2>
        <button class="button" type="submit">

    Save
                </button>
    </div>
    <div class="pb-4">Define how your resource's health should be checked.</div>
    <div class="flex flex-col gap-4">
                <div class="w-32">
            <div class="flex flex-row items-center gap-4 pr-2 py-1 form-control min-w-fit dark:hover:bg-coolgray-100 cursor-pointer">
    <label class="flex gap-4 items-center px-0 min-w-fit label w-full">
        <span class="flex grow gap-2">
                                                Enabled
                                                    </span>
                    <input type="checkbox" class="dark:border-neutral-700 text-coolgray-400 focus:ring-warning dark:bg-coolgray-100 rounded-sm cursor-pointer dark:disabled:bg-base dark:disabled:cursor-not-allowed" wire:loading.attr="disabled" wire:click="instantSave" wire:model="resource.health_check_enabled">
            </label>
</div>
        </div>
        <div class="flex gap-2">
            <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Method
                            <span class="text-helper">*</span>
                                </label>
                <input autocomplete="off" class="input" placeholder="GET" required="" wire:model="resource.health_check_method" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="text" min="" max="" minlength="" maxlength="" id="resource.health_check_method" name="resource.health_check_method">
            </div>
            <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Scheme
                            <span class="text-helper">*</span>
                                </label>
                <input autocomplete="off" class="input" placeholder="http" required="" wire:model="resource.health_check_scheme" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="text" min="" max="" minlength="" maxlength="" id="resource.health_check_scheme" name="resource.health_check_scheme">
            </div>
            <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Host
                            <span class="text-helper">*</span>
                                </label>
                <input autocomplete="off" class="input" placeholder="localhost" required="" wire:model="resource.health_check_host" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="text" min="" max="" minlength="" maxlength="" id="resource.health_check_host" name="resource.health_check_host">
            </div>
            <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Port
                                        <div class="group" helper="If no port is defined, the first exposed port will be used.">
    <div class="info-helper">
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
            </svg>
        
    </div>
    <div class="info-helper-popup">
        <div class="p-4">
            If no port is defined, the first exposed port will be used.
        </div>
    </div>
</div>
                    </label>
                <input autocomplete="off" class="input" placeholder="80" wire:model="resource.health_check_port" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="number" min="" max="" minlength="" maxlength="" id="resource.health_check_port" name="resource.health_check_port">
            </div>
            <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Path
                            <span class="text-helper">*</span>
                                </label>
                <input autocomplete="off" class="input" placeholder="/health" required="" wire:model="resource.health_check_path" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="text" min="" max="" minlength="" maxlength="" id="resource.health_check_path" name="resource.health_check_path">
            </div>
        </div>
        <div class="flex gap-2">
            <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Return Code
                            <span class="text-helper">*</span>
                                </label>
                <input autocomplete="off" class="input" placeholder="200" required="" wire:model="resource.health_check_return_code" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="number" min="" max="" minlength="" maxlength="" id="resource.health_check_return_code" name="resource.health_check_return_code">
            </div>
            <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Response Text
                                </label>
                <input autocomplete="off" class="input" placeholder="OK" wire:model="resource.health_check_response_text" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="text" min="" max="" minlength="" maxlength="" id="resource.health_check_response_text" name="resource.health_check_response_text">
            </div>
        </div>
        <div class="flex gap-2">
            <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Interval (s)
                            <span class="text-helper">*</span>
                                </label>
                <input autocomplete="off" class="input" min="1" placeholder="30" required="" wire:model="resource.health_check_interval" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="number" max="" minlength="" maxlength="" id="resource.health_check_interval" name="resource.health_check_interval">
            </div>
            <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Timeout (s)
                            <span class="text-helper">*</span>
                                </label>
                <input autocomplete="off" class="input" placeholder="30" required="" wire:model="resource.health_check_timeout" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="number" min="" max="" minlength="" maxlength="" id="resource.health_check_timeout" name="resource.health_check_timeout">
            </div>
            <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Retries
                            <span class="text-helper">*</span>
                                </label>
                <input autocomplete="off" class="input" placeholder="3" required="" wire:model="resource.health_check_retries" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="number" min="" max="" minlength="" maxlength="" id="resource.health_check_retries" name="resource.health_check_retries">
            </div>
            <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Start Period (s)
                            <span class="text-helper">*</span>
                                </label>
                <input autocomplete="off" class="input" min="1" placeholder="30" required="" wire:model="resource.health_check_start_period" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="number" max="" minlength="" maxlength="" id="resource.health_check_start_period" name="resource.health_check_start_period">
            </div>
        </div>
    </div>
</form>

alternatively

  1. Navigate to Settings (the instance name is recognized as a username field, which might be a problem with other settings too)
  2. Cick on the "OAuth" tab
  3. The browser extension finds many TOTP fields and opens KeePassXC to unlock the database
HTML snippet and screenshot of one OAuth form section Image
<form wire:submit="submit" class="flex flex-col">
<!-- snip -->
<div class="p-4 border dark:border-coolgray-300 border-neutral-200">
                    <h3>Authentik</h3>
                    <div class="w-32">
                        <div class="flex flex-row items-center gap-4 pr-2 py-1 form-control min-w-fit dark:hover:bg-coolgray-100 cursor-pointer">
    <label class="flex gap-4 items-center px-0 min-w-fit label w-full">
        <span class="flex grow gap-2">
                                                Enabled
                                                    </span>
                    <input type="checkbox" class="dark:border-neutral-700 text-coolgray-400 focus:ring-warning dark:bg-coolgray-100 rounded-sm cursor-pointer dark:disabled:bg-base dark:disabled:cursor-not-allowed" wire:loading.attr="disabled" wire:click="instantSave('authentik')" wire:model="oauth_settings_map.authentik.enabled">
            </label>
</div>
                    </div>
                    <div class="flex flex-col w-full gap-2 xl:flex-row">
                        <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Client ID
                                </label>
                <input autocomplete="off" class="input" wire:model="oauth_settings_map.authentik.client_id" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="text" min="" max="" minlength="" maxlength="" id="oauth_settings_map.authentik.client_id" name="oauth_settings_map.authentik.client_id" placeholder="">
            </div>
                        <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Client Secret
                                </label>
                <div class="relative" x-data="{ type: 'password' }">
                            <div x-on:click="changePasswordFieldType" class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hover:text-white">
                    <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
                        <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
                        <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path>
                        <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6"></path>
                    </svg>
                </div>
                        <input autocomplete="new-password" value="" class="input  pr-[2.8rem]" wire:model="oauth_settings_map.authentik.client_secret" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="password" id="oauth_settings_map.authentik.client_secret" name="oauth_settings_map.authentik.client_secret" placeholder="" aria-placeholder="">

        </div>
            </div>
                        <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Redirect URI
                                </label>
                <input autocomplete="off" class="input" placeholder="https://render-diy.keine-nudel-ist-illegal.de/auth/authentik/callback" wire:model="oauth_settings_map.authentik.redirect_uri" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="text" min="" max="" minlength="" maxlength="" id="oauth_settings_map.authentik.redirect_uri" name="oauth_settings_map.authentik.redirect_uri">
            </div>
                                                                                                    <div class="w-full">
            <label class="flex gap-1 items-center mb-1 text-sm font-medium">Base URL
                                </label>
                <input autocomplete="off" class="input" wire:model="oauth_settings_map.authentik.base_url" wire:dirty.class.remove="dark:focus:ring-coolgray-300 dark:ring-coolgray-300" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" type="text" min="" max="" minlength="" maxlength="" id="oauth_settings_map.authentik.base_url" name="oauth_settings_map.authentik.base_url" placeholder="">
            </div>
                                            </div>
                </div>
<!-- snip -->
</form>

This has happened mutliple times while navigating different pages of coolify, but these are 2 rather simple examples I can find quickly now.

Entering passwords when all features are disabled

  1. Copy the address of your Coolify instance
  2. Add a Site Preference for this address (with a wildcard path)
  3. Disable all features for this site
  4. Navigate to the application you created in the TOTP troubleshooting (or create a new one)
  5. Navigate to the "Danger Zone" configuration page
  6. Click "delete"
  7. "Continue" to delete all related resources
  8. Copy the name from the first field into the second field and confirm with "Continue"
  9. Right-click on the password field, go to the context menu of the KeePassXC browser extension, select "Fill Password Only"
  10. Nothing happens

KeePassXC-Browser Debug Information

KeePassXC - 2.7.10
KeePassXC-Browser - 1.9.9.1
Operating system: Linux x86_64
Browser: Mozilla Firefox 141.0

M3t0r avatar Aug 01 '25 00:08 M3t0r

Easy fix could be to ignore fields that have autocomplete="off"

droidmonkey avatar Aug 01 '25 02:08 droidmonkey