{"id":20066314,"url":"https://github.com/ossobv/openvpn-u2f-setup","last_synced_at":"2026-02-16T19:04:13.764Z","repository":{"id":153495003,"uuid":"334184259","full_name":"ossobv/openvpn-u2f-setup","owner":"ossobv","description":"Using YubiKey U2F as TOTP 2FA for openvpn","archived":false,"fork":false,"pushed_at":"2025-02-25T16:29:12.000Z","size":111,"stargazers_count":2,"open_issues_count":2,"forks_count":0,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-09-20T14:43:34.599Z","etag":null,"topics":["openvpn","u2f","yubikey"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ossobv.png","metadata":{"files":{"readme":"README.rst","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-01-29T15:26:14.000Z","updated_at":"2025-02-25T16:29:17.000Z","dependencies_parsed_at":"2025-02-25T17:25:58.182Z","dependency_job_id":"fa97371a-6092-4145-b7a1-b5d89dd5c630","html_url":"https://github.com/ossobv/openvpn-u2f-setup","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ossobv/openvpn-u2f-setup","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ossobv%2Fopenvpn-u2f-setup","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ossobv%2Fopenvpn-u2f-setup/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ossobv%2Fopenvpn-u2f-setup/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ossobv%2Fopenvpn-u2f-setup/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ossobv","download_url":"https://codeload.github.com/ossobv/openvpn-u2f-setup/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ossobv%2Fopenvpn-u2f-setup/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29515548,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-16T18:37:19.720Z","status":"ssl_error","status_checked_at":"2026-02-16T18:36:46.920Z","response_time":115,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["openvpn","u2f","yubikey"],"created_at":"2024-11-13T13:56:08.137Z","updated_at":"2026-02-16T19:04:13.726Z","avatar_url":"https://github.com/ossobv.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"openvpn-u2f-setup\n=================\n\nConfiguration and howto to use a *U2F device (YubiKey)* as time based second\nauthentication factor for *OpenVPN* logins.\n\nComponents\n----------\n\n* OpenVPN server:\n\n  - ``openvpn`` daemon, with an already sane configuration and proper\n    certificates;\n\n  - ``u2f-server`` command line tool to verify the challenge signature;\n\n  - an ``auth-user-pass-verify`` script that receives the *U2F* key handle\n    *as username* and the challenge and response *as password*:\n    `\u003copenvpn-u2f-verify\u003e`_\n\n* OpenVPN client:\n\n  - ``openvpn`` daemon, with an already sane configuration and proper\n    certificates;\n\n  - a *YubiKey* or other *U2F/FIDO2* device;\n\n  - ``u2f-host`` command line tool to sign a challenge based on the\n    current timestamp;\n\n  - a `SystemD ask-password agent\n    \u003chttps://systemd.io/PASSWORD_AGENTS/\u003e`_ (external link) to pick up\n    ``auth-user-pass`` requests from *OpenVPN*:\n    `\u003copenvpn-u2f-ask-password\u003e`_\n\n    *(Do not forget to add this option to the client config. The OpenVPN\n    client will not complain, but simply fail to set up the VPN. Even\n    clients with explicitly disabled U2F need to provide something.)*\n\nThe ``u2f-host(1)`` and ``u2f-server(1)`` CLI applications are in charge\nof the *U2F* heavy lifting. The *key handles*, challenges and\nresponses are sent to the *OpenVPN* server using the *username* and\n*password* additional authentication.\n\n**Personal certificates are your first line of defense. U2F is the second.**\n\n\nCaveats\n-------\n\n**The challenge is not random.** But because it uses the unixtime which\nshould be the same everywhere, we use that to guard against replay attacks\n(outside a short time window). This caveat is due to the fact that\n*OpenVPN* does not support passing a challenge to the client. If it did,\nwe could pass the required *key handle* and supply a truly random\nchallenge.\n\n*Note that using the timestamp is no less secure than the ubiqitous\ntime-based one-time passwords (TOTP) as provided by the Google\nAuthenticator and Authy apps.* And on all other fronts, using a hardware\n*U2F device* with a public/private key is *more* secure because *there\nis no shared secret* and *the private key cannot be extracted from the\nhardware device.*\n\n\nHow does this compare to other solutions?\n-----------------------------------------\n\n* You can use the `old style Yubico OTP\n  \u003chttps://developers.yubico.com/yubico-pam/YubiKey_and_OpenVPN_via_PAM.html\u003e`_\n  (external link) far more easily. It's drawback however is that unless\n  you set up a *Yubico OTP* server locally, your service has to have\n  access to the internet, relying on *their* service. That is not good\n  enough if you rely on this access to *fix* internet connectivity\n  problems.\n\n* *SparkLabs* has some kind of `modified OpenVPN server + plugins\n  \u003chttps://www.sparklabs.com/support/kb/article/yubikey-u2f-two-factor-authentication-with-openvpn-and-viscosity/\u003e`_\n  (external link) which requires (a) a patched *OpenVPN*, (b) an equally\n  complicated setup and (c) it has no documentation on `how an OpenVPN\n  client should present these U2F credentials\n  \u003chttps://github.com/thesparklabs/openvpn-two-factor-extensions/blob/73166ce305260bf0baa4381f98330bb82c36447c/yubikey-u2f-pam-plugin/auth-pam-u2f.py#L66-L96\u003e`_\n  (source code) at all. (Their *Viscosity* product is proprietary and\n  works only on Windows and Mac.)\n\n\nHow does this work?\n-------------------\n\nAt connect time ``openvpn-u2f-setup`` calls ``u2f-host`` to sign the\ncurrent timestamp using the ``-a authenticate`` action.\n\nThis goes in::\n\n    {\"keyHandle\": \"\u003c86-byte-b64encoded-handle\u003e\",\n     \"version\": \"U2F_V2\",\n     \"challenge\": \"\u003c43-byte-b64encoded-challenge\u003e\",\n     \"appId\": \"\u003cAPPID\u003e\"}\n\nThe client knows which handle it's supposed to use (from\n``keyhandle.dat``). The challenge is constructed from the current\ntimestamp.\n\nThe *U2F device* signs the challenge, and responds with something like this::\n\n    {\"signatureData\": \"\u003c102-byte-encoded-signed-respons\u003e\",\n     \"clientData\": \"\u003cN-byte-64encoded-json-which-was-signed\u003e\",\n     \"keyHandle\": \"\u003c86-byte-b64encoded-handle\u003e\"}\n\nThe ``openvpn-u2f-ask-password`` will pass these to the *OpenVPN* client\n(which in turn passes them on to the server):\n\n* ``username = KEYHANDLE`` (the 86 byte handle)\n\n* ``password = TIMESTAMP '/' SIGNATURE`` (10 digit timestamp, 1 slash,\n  and about 102 bytes of signature)\n\nThese both fit in the 256 bytes maximum of *OpenVPN*\n``OPTION_PARM_SIZE``, so no changes to *OpenVPN* are needed.\n\nOn the server side, the same challenge is reconstructed from the known\n*key handle* and *timestamp* and the signature is validated against the\n(known) public key (``userkey.dat``).\n\n\nOpenVPN server setup\n--------------------\n\nserver.conf:\n\n::\n\n    # [OSSO B.V. openvpn-u2f-setup]\n    # (Use via-file because we'd have to set --script-security 3 for via-env.)\n    auth-user-pass-verify /etc/openvpn/openvpn-u2f-setup/openvpn-u2f-verify via-file\n    reneg-sec 28800  # 8 hours so we don't need to renegotiate U2F too often\n    # Or, if you insist on long lived sessions with only a single interaction:\n    #auth-gen-token  # an alternative to longer reneg-sec\n\nFurther, you'll need to create a ``keyhandle.dat`` and ``userkey.dat``\nand place them in ``/etc/openvpn/u2f/\u003cCN\u003e/`` where ``\u003cCN\u003e`` is the\ncertificate *commonName*. See CREATING HANDLES below.\n\nAnd ``/etc/openvpn/openvpn-u2f-setup/openvpn-u2f-verify`` needs to work. It\nwill mainly require you to have ``u2f-server(1)`` installed.\n\n\nOpenVPN client setup\n--------------------\n\nclient.conf:\n\n::\n\n    # [OSSO B.V. openvpn-u2f-setup]\n    # When OpenVPN is tied to SystemD, this will trigger ask-password support.\n    # You'll need to have the openvpn-u2f-ask-password daemon available to\n    # notify you that U2F authentication is needed and interact with it.\n    auth-user-pass\n    auth-nocache        # caching the one time password is pointless\n    auth-retry interact # force user action before reconnection attempt\n    reneg-sec 0         # let the server decide when to renegotiate keys\n\nFor clients where *U2F* is explicitly disabled, you will still need dummy\ncredentials::\n\n    # [OSSO B.V. openvpn-u2f-setup]\n    # Dummy auth user/pass for VPN clients with U2F explicitly disabled.\n    # (Any file with 2 or more lines will do.)\n    auth-user-pass /etc/protocols\n    auth-retry nointeract\n\n\nCREATING HANDLES\n----------------\n\nOn your laptop/desktop, ``u2f-host(1)`` needs to be installed. It will\nhandle the communication with the *U2F device (YubiKey)* through the\n``openvpn-u2f-ask-password`` helper.\n\nWhen configuring the *U2F* support, you will need to run a registration\nstep, preferably directly on the *OpenVPN* server:\n\n.. code-block:: console\n\n    # CN=yourCommonName \u0026\u0026 mkdir -p /etc/openvpn/u2f/$CN\n    # ORIGIN=pam://openvpn-server \u0026\u0026 APPID=openvpn\n    # umask 0077\n\n.. code-block:: console\n\n    # u2f-server -a register -o $ORIGIN -i $APPID \\\n        -k /etc/openvpn/u2f/$CN/keyhandle.dat \\\n        -p /etc/openvpn/u2f/$CN/userkey.dat\n    { \"challenge\": \"nO72...\", \"version\": \"U2F_V2\", \"appId\": \"openvpn\" }\n\nFeed this challenge (the entire JSON blob) to ``u2f-host``:\n\n.. code-block:: console\n\n    $ ORIGIN=pam://openvpn-server\n\n.. code-block:: console\n\n    $ u2f-host -a register -o $ORIGIN \u003c\u003cEOF\n    { \"challenge\": \"nO72...\", \"version\": \"U2F_V2\", \"appId\": \"openvpn\" }\n    EOF\n\nNow touch the *U2F device*. The ``u2f-host`` will output something like this:\n\n.. code-block:: data\n\n    { \"registrationData\": \"BQS...\", \"clientData\": \"eyAiY...\" }\n\nFeed the reponse (the entire JSON blob) back to the ``u2f-server``, and end\n*stdin* with a ^D (control-D).\n\nIt will say ``Registration successful`` and you should now have two files:\n\n.. code-block:: console\n\n    # ls -l /etc/openvpn/u2f/$CN\n    total 8\n    -rw------- 1 root root 86 jan 29 17:47 keyhandle.dat\n    -rw------- 1 root root 65 jan 29 17:47 userkey.dat\n\n**NOTE: If your openvpn server runs as the openvpn user, make sure the\nkey files on the server are readable by the auth-user-pass-verify\nscript:**\n\n.. code-block:: console\n\n    # chown -R openvpn: /etc/openvpn/u2f/$CN\n\nA handle consists of printable characters. You'll need this *key handle* on\nthe client side as well. See below at `Configuring the ask-password\nhelper on the client`_.\n\n.. code-block:: console\n\n    # cat /etc/openvpn/u2f/$CN/keyhandle.dat\n    b6Ac2BI...\n\n(For the curious: the `details of the registrationData layout\n\u003chttps://fidoalliance.org/specs/u2f-specs-1.0-bt-nfc-id-amendment/fido-u2f-raw-message-formats.html#registration-response-message-success\u003e`_\n(external link) or `example registrationData extraction\n\u003chttps://github.com/Yubico/python-u2flib-server/blob/b2053563d4cdd530f254a863e59af11235bfde8f/u2flib_server/model.py#L156-L164\u003e`_\n(source code).)\n\n\nConfiguring the ask-password helper on the client\n-------------------------------------------------\n\n* Install `\u003copenvpn-u2f-ask-password\u003e`_ (or simply this repository) in\n  ``/etc/openvpn/openvpn-u2f-setup/``::\n\n    cd /etc/openvpn\n    git clone https://github.com/ossobv/openvpn-u2f-setup.git\n\n* Set the the *OpenVPN* configuration as found in `OpenVPN client setup`_.\n\n* Copy your personal ``keyhandle.dat`` from the server. If you run the\n  `\u003copenvpn-u2f-ask-password.service\u003e`_ as a user, call it\n  ``~/.u2f_keys`` (a file). Otherwise call it\n  ``/etc/openvpn/client/VPN_NAME/keyhandle.dat`` (assuming ``VPN_NAME.conf``\n  holds your VPN config).\n\n* Ensure that your have all dependencies (``python3-pyinotify`` and\n  optionally ``python3-gi`` for *GNOME* notification integration, and\n  ``u2f-host`` to for communication with the *U2F device*)::\n\n    apt-get install python3-pyinotify python3-gi u2f-host\n\n* Configure so it auto-starts, using *SystemD* (see\n  `\u003copenvpn-u2f-ask-password.service\u003e`_)::\n\n    systemctl start [--user] openvpn-u2f-ask-password.service\n    systemctl enable [--user] openvpn-u2f-ask-password.service\n\n\nRunning\n-------\n\nIf everything is properly configured, a restart of your VPN connection\nshould trigger a blinking light on your *U2F device (YubiKey)*. Touch it\nto log in.\n\nOr don't touch it, and confirm that you cannot log in.\n\nWhile testing, you can start ``openvpn-u2f-ask-password`` from the\ncommand line to get a better feel of what's going on.\n\n\nF.A.Q.\n======\n\n* How do I know when to touch the *U2F device*?\n\n  If you're using *GNOME* on *Ubuntu/Focal*, it should look somewhat\n  like this:\n\n  .. image:: ./openvpn-u2f-ask-password.gif\n    :alt: GUI notification on right side\n\n  Right now, you probably need to have ``notify-osd`` installed.\n\n* When doing a ``openvpn`` restart, I get a ``Enter Auth Username:`` shown.\n\n  This is an unfortunate effect of having multiple ``systemd-ask-password``\n  agents. You can ignore the console version when\n  ``openvpn-u2f-ask-password`` works as intended. The console agent will\n  automatically close/abort once our agent has provided the credentials.\n\n* After touching the *U2F device*, permission is needed to pass the\n  username and password data back to the requestor. This means you might\n  get prompted by the *PolicyKit Authentication Agent* twice. See the\n  ``openvpn-u2f-ask-password.pkla`` or\n  ``openvpn-u2f-ask-password.rules`` files for fixes.\n\n* ``openvpn-u2f-ask-password`` reports: ``error (-6): authenticator error``\n\n  This generally means one of two things:\n\n  - the *wrong U2F device* is inserted, or\n\n  - the *key handle* was generated for a different APPID\n    (did you change it after performing the registration step?)\n\n* ``u2f-host`` claims my *YubiKey* is not an *U2F device*:\n\n  .. code-block:: console\n\n    $ u2f-host -a register -o pam://openvpn-server\n    { \"challenge\": \"VIrN...\", \"version\": \"U2F_V2\", \"appId\": \"openvpn\" }\n    ^D\n    error: u2fh_devs_discover (-5): cannot find U2F device\n\n  This is might be because it is not enabled. See ``ykman(1)`` (from the\n  *yubikey-manager* package):\n\n  .. code-block:: console\n\n    $ ykman mode\n    Current connection mode is: OTP+CCID\n    Supported USB interfaces are: OTP, FIDO, CCID\n\n  .. code-block:: console\n\n    $ ykman mode OTP+FIDO+CCID\n    Set mode of YubiKey to OTP+FIDO+CCID? [y/N]: y\n    Mode set! You must remove and re-insert your YubiKey for this change\n    to take effect.\n\n\nBUGS/TODO\n=========\n\n* See if we want to use the server-side openvpn management interface of\n  ``--management`` through which we can send a ``client-deny`` command\n  which takes a ``client_reason`` through which we could push a\n  challenge... see: ./doc/management-notes.txt in the openvpn tree.\n\n* Fix better sane /u2f/ keyhandle.dat paths:\n\n  - for less confusion (difference between client and server)\n\n  - for multiple key handles for a single $CN\n\n* Improve openvpn-u2f-ask-password usage for non-root users (@Urth?).\n\n* We may want to add some wrapper scripts to make life\n  managing/registering keys and handles easier. (We can manage quite a\n  bit by decoding the ``registrationData`` ourself.)\n\n* When everything is well-tested and works, we'll need to swap the\n  default of ignoring users without ``keyhandle.dat`` to prohibit. We'll\n  probably still want some way to allow certain certificates to connect\n  without *U2F* though. (For systems where there is no human interaction.)\n\n* Document why you'd want to be root. And what you need to not be root.\n  (umask? Or fix key-read permissions to the openvpn-user?)\n\n* For systems where systemd-ask-password does not work well, we could\n  see if we can abuse auth-user-pass to read from a pseudofile (pipe?\n  special filesystem?). Benefit: more granular control. Drawback: more\n  complicated.\n\n* If we wanted, we could use a side-channel (https?) to request a\n  challenge during authentication. If set up properly, this could be\n  more secure (because of the random challenge), but it does complicate\n  the setup. (*SparkLabs Viscosity* appears to use custom *OpenVPN*\n  `client reject reason\n  \u003chttps://github.com/thesparklabs/openvpn-two-factor-extensions/blob/73166ce305260bf0baa4381f98330bb82c36447c/yubikey-u2f-pam-plugin/auth-pam-u2f.c#L487-L497\u003e`_\n  for this purpose.)\n\n* Note that ``auth-token`` is not something we could use to pass a\n  challenge to the connecting client. This is a token that is used for\n  *renegotiation*. See ``auth-gen-token`` in the manual.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fossobv%2Fopenvpn-u2f-setup","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fossobv%2Fopenvpn-u2f-setup","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fossobv%2Fopenvpn-u2f-setup/lists"}