Help getting azure vpn to work

I’ve done a lot of the work and packaged the proprietary azure VPN from the deb file they provide for Ubuntu. But now I’ve hit a roadblock.

I can successfully authorize myself, but then I get a certificate error. I suspect that is, because I can’t select a certificate here:

image

The dropdown is empty, and I cannot open it, this is what it looks like on Ubuntu:
image
image

So I’ve tried all sorts of things, but I suspect the issue might be, that NixOS doesn’t populate all the certificates at /etc/ssl/certs, but only a bundle. I did an strace:

strace -f -e trace=open,openat,stat,stat64 ./result/bin/microsoft-azurevpnclient > strace.log 2>&1

And found that the program seems to be accessing the certificate directory:

[pid 28889] openat(AT_FDCWD, "/etc/ssl/certs/", O_RDONLY|O_DIRECTORY) = 24

I found that the cacert package has an unbundled output. And I’ve tried supplying that to the NixOS configuration, just to get it to run. I’ve even ended up deleting all the contents of the directory, and doing:

  environment.etc."ssl/certs".source = "${pkgs.cacert.unbundled}/etc/ssl/certs/*";

Not even that helped… Does anyone have an idea what might be happening here?

Here is my derivation, don't make too much fun of it :smiley:
{
  lib,
  stdenv,
  fetchurl,
  dpkg,
  autoPatchelfHook,
  makeWrapper,
  openssl,
  gtk3,
  libsecret,
  cairo,
  nss,
  nspr,
  libuuid,
  at-spi2-core,
  libdrm,
  mesa,
  gtk2,
  glib,
  pango,
  atk,
  curl,
  zenity,
  cacert,
  openvpn,
# libxcb,
# cairo-xcb,
# libX11,
# libXcomposite,
# libXdamage,
# libXext,
# libXfixes,
# libXrandr,
# libxkbcommon,
# libxshmfence,
}:

stdenv.mkDerivation rec {
  pname = "microsoft-azurevpnclient";
  version = "3.0.0";

  src = fetchurl {
    url = "https://packages.microsoft.com/ubuntu/22.04/prod/pool/main/m/microsoft-azurevpnclient/microsoft-azurevpnclient_${version}_amd64.deb";
    hash = "sha256-nl02BDPR03TZoQUbspplED6BynTr6qNRVdHw6fyUV3s=";
  };

  runtimeDependencies = [ zenity ];

  nativeBuildInputs = [
    dpkg
    autoPatchelfHook
    makeWrapper
  ];

  buildInputs = [
    zenity
    openssl
    gtk3
    libsecret
    cairo
    # libxcb
    nss
    nspr
    libuuid
    stdenv.cc.cc.lib
    at-spi2-core
    libdrm
    mesa
    gtk2
    glib
    pango
    atk
    curl
    cacert # Add this
    openvpn

    # cairo-xcb
    # libX11
    # libXcomposite
    # libXdamage
    # libXext
    # libXfixes
    # libXrandr
    # libxkbcommon
    # libxshmfence
  ];

  unpackPhase = ''
    dpkg-deb -x $src .
  '';

  # addAutoPatchelfSearchPath ${jre8}/lib/openjdk/jre/lib/
  # preBuild = ''
  #   addAutoPatchelfSearchPath opt/microsoft/microsoft-azurevpnclient/lib
  # '';

  # runtimeDependencies = [ "$out/lib" ];

  installPhase = ''
    mkdir -p $out
    cp -r opt $out
    cp -r usr/* $out

    mkdir -p $out/bin

    ln -s $out/opt/microsoft/microsoft-azurevpnclient/microsoft-azurevpnclient $out/bin/microsoft-azurevpnclient
    ln -s $out/opt/microsoft/microsoft-azurevpnclient/lib $out

    wrapProgram $out/bin/microsoft-azurevpnclient \
      --prefix PATH : "${openvpn}/bin" \
      --prefix PATH : "${zenity}/bin" \
      --prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath buildInputs} \
      --prefix LD_LIBRARY_PATH : "$out/lib"
    # TODO:
    # Fix desktop file location
    # mkdir -p $out/share/applications
    # mv $out/share/applications/azurevpnclient.desktop $out/share/applications/
  '';

  meta = {
    description = "Microsoft Azure VPN Client";
    homepage = "https://azure.microsoft.com/en-us/services/vpn-gateway/";
    # TODO:
    # license = licenses.unfree;
    platforms = [ "x86_64-linux" ];
    maintainers = [ ];
  };
}

Here some additional information that might be less important:

The error I get when trying to connect “without” a certificate selected:

Feb 14 18:11:29 callisto AzureVPNClient[8262]: TId:[8547] No cert verification callback from client
Feb 14 18:11:29 callisto AzureVPNClient[8262]: TId:[8547] Invalid certificate data at index 0
Feb 14 18:11:29 callisto AzureVPNClient[8262]: TId:[8547] Verification result for certificate chain: 0
Feb 14 18:11:29 callisto AzureVPNClient[8262]: TId:[8547] [Primary] OPENVPNFRAMING: OpenVpnFraming hit error processing packet, initiating teardown of tunnel        error: 610970100000012 from tls_openssl_common.cpp line 151, facility MobileAccess with detail: Root cert validation failed
Feb 14 18:11:29 callisto AzureVPNClient[8262]: TId:[8547] OPENVPNBUILDER:Terminating datapath connection as tcp connection is terminated.
Feb 14 18:11:29 callisto AzureVPNClient[8262]: TId:[8547] [Primary] OPENVPNCONNECTIONSTATE: Changing state to failed
Feb 14 18:11:29 callisto AzureVPNClient[8262]: TId:[8547] Adding Control path state change event
Feb 14 18:11:29 callisto AzureVPNClient[8262]: TId:[8383] OPENVPNBUILDER:Connection Failed! Last OpenVpnSessionState:  0
Feb 14 18:11:29 callisto AzureVPNClient[8262]: TId:[8547] Xpoll fds destroyed
Feb 14 18:11:29 callisto AzureVPNClient[8262]: TId:[8547] [Primary] FDTRANSPORT: OS closed

This seems to be only the start of a longer error chain, though, I can provide more if it’s interesting.

Microsoft specifically states on their website, that this dropdown should not be left empty, that’s why I’m pursuing this:

View the connection profile information. Change the Certificate Information value to show the default DigiCert_Global_Root G2.pem or DigiCert_Global_Root_CA.pem. Don’t leave blank.

Does strace show whether the vpn client is trying to open ca-bundle.crt or ca-certificates.crt files? If it is, but does not like the content, might be worth trying security.pki.useCompatibleBundle = true option.

If it’s adamant about having individual pem files (according to the liveCD that’s what Ubuntu has), you could try:

  1. Setting environment variable SSL_CERT_DIR to ${pkgs.cacert.unbundled}/etc/ssl/certs – vpn client might just respect it

  2. Setting a single file in an option and checking if the client starts seeing certificates (chosen at random here):

    environment.etc."ssl/certs/DigiCert_Global_Root_G2:33af1e6a711a9a0bb2864b11d09fae5.crt".source = "${pkgs.cacert.unbundled}/etc/ssl/certs/DigiCert_Global_Root_G2:33af1e6a711a9a0bb2864b11d09fae5.crt";

  3. Some software relies on file extension without really checking the content of the file. Ubuntu live cd has all certificates with pem extension (which can really mean anything) whereas the files in the cacert have crt extension (which indicates that it’s a certificate). In that case try

    environment.etc."ssl/certs/DigiCert_Global_Root_G2:33af1e6a711a9a0bb2864b11d09fae5.pem".text = ''
         <source of the certificate from a trusted place, don\'t copypaste it from a post on an Internet forum>
    '';
    

Your last suggestion “worked”! I didn’t even provide sensible content. I just added the exact example you provided. And it showed up in the dropdown! Would there be some sort of better solution, than copying all the certificates… is there an “unpacked pem” package?

Then looks like they are really just looking at the extension and hardcoding the path.

You can enumerate files by hand in environment.etc.<filename>.[text|source] and expose some wanted certificates this way. This is the easiest option but you’d need to maintain the certificate content when it changes (certs expire).

You could also run the VPN client in a namespace which would shadow the system’s /etc/ssl/certs by a bind mount. The bind mount would, in turn, point to a store path that would basically take all files from pkgs.cacert.unbundled and change the extensions. This option is a bit more complex than the previous one.

Yet another alternative solution is to use import from derivation to iterate over files from pkgs.cacert.unbundled, get them back as nix values and apply them as configuration option. See that page why you might not want to do this. Details tag has the code.

There may be alternative solutions though, but I can’t think of any off the top of my head. And I really wanted to write some IFD code :slight_smile:

This code is runnable as `nix run .#checks.x86_64-linux.test.driverInteractive`. It uses IFD to build cacert.unbundled, then iterates over built directory and constructs the attrset for `environment.etc`.

I am using pipe operator to maintain code clarity.

{
  description = "Sample check flake";

  inputs = {
    nixpkgs.url = "nixpkgs/nixos-24.11";
  };

  outputs =
    inputs@{ self, nixpkgs, ... }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in
    {
      # Runnable via
      # ❯ nix build -L .\#checks.x86_64-linux.test
      checks.${system}.test = pkgs.testers.runNixOSTest {
        name = "test";
        node.specialArgs = {
          inherit (self) inputs outputs;
        };
        nodes.machine1 =
          {
            config,
            pkgs,
            lib,
            ...
          }:
          {
            services.getty.autologinUser = "alice";
            users.users.alice = {
              isNormalUser = true;
              password = "hunter2";
            };

            environment.etc =
              # The trick is import from derivation
              # https://nix.dev/manual/nix/2.25/language/import-from-derivation
              # This is pretty slow for the reasons outlined on that page
              # But hey, it works!
              pkgs.cacert.unbundled
              |> (it: it + "/etc/ssl/certs")
              |> builtins.readDir # Produce an attrset with names representing the files
              # Construct an attrset with files content and proper file names and paths
              |> lib.mapAttrs' (
                name: _:
                lib.nameValuePair
                  (
                    name
                    # Replace the extension. This might be too simplistic, but it works
                    |> builtins.replaceStrings [ ".crt" ] [ ".pem" ]
                    # Prepend `ssl/certs` so they end up in the right location
                    |> (it: "ssl/certs/${it}")
                  )
                  # The attribute values will have the contents of the certificates
                  (pkgs.cacert.unbundled |> (it: it + "/etc/ssl/certs/${name}") |> builtins.readFile)
              )
              # Assemble it all back
              |> lib.mapAttrs (_: value: { text = value; });
          };
        # If developing a proper test script, see
        # https://nixos.org/manual/nixos/stable/#ssec-machine-objects
        testScript = "start_all()";
      };
    };
}