Hi Peter,
Thank you for the review.
On Fri, Nov 21, 2025 at 12:13:09PM +0100, Peter Krempa wrote:
> On Thu, Nov 20, 2025 at 22:23:44 +0530, Arun Menon via Devel wrote:
> > A new configuration file called secrets.conf is introduced to
> > let the user configure the path to the secrets encryption key.
> > This key will be used to encrypt/decrypt the secrets in libvirt.
> >
> > By default the path is set to the runtime directory
> > /run/libvirt/secrets, and it is commented in the config file.
> > After parsing the file, the virtsecretd driver checks if an
> > encryption key is present in the path and is valid.
> >
> > By default, if no encryption key is present in the path, then
> > the service will by default use the encryption key stored in the
> > CREDENTIALS_DIRECTORY.
> >
> > Add logic to parse the encryption key file and store the key.
> > It also checks for the encrypt_data attribute in the config file.
> > The encryption and decryption logic will be added in the subsequent patches.
> >
> > Signed-off-by: Arun Menon <[email protected]>
> > ---
> > libvirt.spec.in | 3 +
> > po/POTFILES | 1 +
> > src/conf/meson.build | 1 +
> > src/conf/secret_config.c | 207 +++++++++++++++++++++++++
> > src/conf/secret_config.h | 48 ++++++
> > src/libvirt_private.syms | 2 +
> > src/secret/libvirt_secrets.aug | 40 +++++
> > src/secret/meson.build | 18 +++
> > src/secret/secrets.conf.in | 12 ++
> > src/secret/test_libvirt_secrets.aug.in | 6 +
> > 10 files changed, 338 insertions(+)
> > create mode 100644 src/conf/secret_config.c
> > create mode 100644 src/conf/secret_config.h
> > create mode 100644 src/secret/libvirt_secrets.aug
> > create mode 100644 src/secret/secrets.conf.in
> > create mode 100644 src/secret/test_libvirt_secrets.aug.in
>
> [...]
>
> > diff --git a/src/conf/secret_config.c b/src/conf/secret_config.c
> > new file mode 100644
> > index 0000000000..a1c9b6bc2f
> > --- /dev/null
> > +++ b/src/conf/secret_config.c
> > @@ -0,0 +1,207 @@
> > +/*
> > + * secret_config.c: secrets.conf config file handling
> > + *
> > + * Copyright (C) 2025 Red Hat, Inc.
> > + *
> > + * This library is free software; you can redistribute it and/or
> > + * modify it under the terms of the GNU Lesser General Public
> > + * License as published by the Free Software Foundation; either
> > + * version 2.1 of the License, or (at your option) any later version.
> > + *
> > + * This library is distributed in the hope that it will be useful,
> > + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> > + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> > + * Lesser General Public License for more details.
> > + *
> > + * You should have received a copy of the GNU Lesser General Public
> > + * License along with this library. If not, see
> > + * <http://www.gnu.org/licenses/>.
> > + */
>
> For new files we usually use the SPDX header:
>
> /*
> * SPDX-License-Identifier: LGPL-2.1-or-later
> */
>
> instead of the full blob above.
Yes, will do. Thanks
>
> > +
> > +#include <config.h>
> > +#include <fcntl.h>
> > +#include "configmake.h"
> > +#include "datatypes.h"
> > +#include "virlog.h"
> > +#include "virerror.h"
> > +#include "virfile.h"
> > +#include "virutil.h"
> > +#include "secret_config.h"
> > +
> > +
> > +#define VIR_FROM_THIS VIR_FROM_CONF
> > +
> > +VIR_LOG_INIT("secret.secret_config");
> > +
> > +static virClass *virSecretDaemonConfigClass;
> > +static void virSecretDaemonConfigDispose(void *obj);
> > +
> > +static int
> > +virSecretConfigOnceInit(void)
> > +{
> > + if (!VIR_CLASS_NEW(virSecretDaemonConfig, virClassForObject()))
> > + return -1;
> > +
> > + return 0;
> > +}
> > +
> > +VIR_ONCE_GLOBAL_INIT(virSecretConfig);
> > +
> > +int
> > +virSecretDaemonConfigFilePath(bool privileged, char **configfile)
> > +{
> > + if (privileged) {
> > + *configfile = g_strdup(SYSCONFDIR "/libvirt/secrets.conf");
> > + } else {
> > + g_autofree char *configdir = NULL;
> > +
> > + configdir = virGetUserConfigDirectory();
> > +
> > + *configfile = g_strdup_printf("%s/secrets.conf", configdir);
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +static int
> > +virSecretLoadDaemonConfig(virSecretDaemonConfig *cfg,
> > + const char *filename)
> > +{
> > + g_autoptr(virConf) conf = NULL;
> > +
> > + if (access(filename, R_OK) == 0) {
>
> Use virFileExists instead of open-coding.
Yes, will do.
>
> > + conf = virConfReadFile(filename, 0);
> > + if (!conf)
> > + return -1;
> > + if (virConfGetValueInt(conf, "encrypt_data", &cfg->encrypt_data) <
> > 0) {
> > + virReportError(VIR_ERR_INTERNAL_ERROR,
> > + _("Failed to get encrypt_data from %1$s"),
> > + filename);
>
> Not an internal error. VIR_ERR_CONF_SYNTAX
yes, will change it.
>
> Also encrypt_data really ought to be a bool based on usage. Here you'll
> have to convert it properly on parsing.
I will change it to boolean.
>
> > + return -1;
> > + }
> > +
> > + if (virConfGetValueString(conf, "secrets_encryption_key",
> > + &cfg->secretsEncryptionKeyPath) < 0) {
> > + virReportError(VIR_ERR_INTERNAL_ERROR,
> > + _("Failed to get secrets_encryption_key from
> > %1$s"),
> > + filename);
>
> ditto
>
> > + return -1;
> > + }
> > + }
> > + return 0;
> > +}
> > +
> > +static bool getSecretsEncryptionKey(virSecretDaemonConfig *cfg,
> > + uint8_t **secrets_encryption_key,
> > size_t *secrets_encryption_keylen)
>
> This function doesn't follow the naming convention and coding style.
I think virSecretGetEncryptionKey should be okay?
>
> > +{
> > + int fd = -1;
>
> Declare this using VIR_AUTOCLOSE so that you don't have to deal with it
> in this whole file.
Sure, thanks.
>
> > + struct stat st;
> > +
> > + if ((fd = open(cfg->secretsEncryptionKeyPath, O_RDONLY)) < 0) {
> > + virReportError(VIR_ERR_INTERNAL_ERROR, _("Cannot open secrets key
> > file '%1$s'"),
> > + cfg->secretsEncryptionKeyPath);
> > + return false;
> > + }
>
> I'd suggest you factor out the useful bits from
> 'qemuDomainMasterKeyReadFile' and then reuse it rather than reimplement
> this
Yes, will do.
>
>
> > + if (fstat(fd, &st) < 0) {
> > + virReportError(VIR_ERR_INTERNAL_ERROR, _("Cannot stat secrets key
> > file '%1$s'"),
> > + cfg->secretsEncryptionKeyPath);
> > + VIR_FORCE_CLOSE(fd);
>
> With the autoclose helper this won't be needed.
Yes.
>
>
> > + return false;
> > + }
> > + *secrets_encryption_keylen = st.st_size;
> > + if (*secrets_encryption_keylen == 0) {
> > + virReportError(VIR_ERR_INTERNAL_ERROR, _("Secrets encryption key
> > file %1$s is empty"),
> > + cfg->secretsEncryptionKeyPath);
> > + VIR_FORCE_CLOSE(fd);
> > + return false;
> > + }
> > + *secrets_encryption_key = g_new0(uint8_t, *secrets_encryption_keylen);
>
> This allocates buffer based on the actual size, IMO yoiu should limit it
> to something more sensible.
I will #define the key length and limit the secrets_encryption_key to that size.
>
> > + if (saferead(fd, &secrets_encryption_key, *secrets_encryption_keylen)
> > != *secrets_encryption_keylen) {
> > + virReportError(VIR_ERR_INTERNAL_ERROR, _("Cannot read secrets key
> > file '%1$s'"),
> > + cfg->secretsEncryptionKeyPath);
> > + VIR_FORCE_CLOSE(fd);
> > + return false;
>
> For functions using libvirt errors please stick to the '0' '-1' return
> value convntion.
Yes. Will change it.
>
> > + }
> > + VIR_FORCE_CLOSE(fd);
> > + if (*secrets_encryption_keylen != 32) {
> > + virReportError(VIR_ERR_INTERNAL_ERROR, _("Secrets encryption key
> > file %1$s must be 32 bytes"),
> > + cfg->secretsEncryptionKeyPath);
> > + return false;
> > + }
> > + return true;
> > +}
> > +
> > +virSecretDaemonConfig *
> > +virSecretDaemonConfigNew(bool privileged)
> > +{
> > + g_autoptr(virSecretDaemonConfig) cfg = NULL;
> > + g_autofree char *configdir = NULL;
> > + g_autofree char *configfile = NULL;
> > + const char *credentials_directory;
> > +
> > + if (virSecretConfigInitialize() < 0)
> > + goto error;
> > +
> > + if (!(cfg = virObjectNew(virSecretDaemonConfigClass)))
> > + goto error;
> > +
> > + cfg->secretsEncryptionKeyPath = NULL;
> > +
> > + if (privileged) {
> > + configdir = g_strdup(SYSCONFDIR "/libvirt");
> > + } else {
> > + g_autofree char *rundir = virGetUserRuntimeDirectory();
> > + configdir = virGetUserConfigDirectory();
> > + }
> > + configfile = g_strconcat(configdir, "/secrets.conf", NULL);
>
> This open-codes what the unused 'virSecretDaemonConfigFilePath' function
> does. Either use the function or delete it.
Will use the function. Also I noticed rundir is not really used here.
>
> > + if (virSecretLoadDaemonConfig(cfg, configfile) < 0)
> > + goto error;
> > +
> > + if (!(credentials_directory = getenv("CREDENTIALS_DIRECTORY"))) {
> > + credentials_directory = NULL;
>
> This is a pointless condition. If getenv("CREDENTIALS_DIRECTORY")
> returns NULL you set it to NULL.
Yes.
>
>
> > + }
> > +
> > + if (cfg->secretsEncryptionKeyPath) {
> > + VIR_DEBUG("Secrets encryption key path: %s",
> > cfg->secretsEncryptionKeyPath);
> > + } else if (credentials_directory) {
> > + VIR_DEBUG("Using credentials directory from environment: %s",
> > + credentials_directory);
> > + cfg->secretsEncryptionKeyPath =
> > g_strdup_printf("%s/secrets-encryption-key",
> > +
> > credentials_directory);
> > + } else {
> > + VIR_DEBUG("No secrets encryption key found in credentials
> > directory");
> > + cfg->secretsEncryptionKeyPath = NULL;
> > + }
>
> This logic is a bit convoluted. I suggest you do an unconditional log of
> the actual path after you determine whic one to use:
>
> if (!cfg->secretsEncryptionKeyPath && credentials_directory) {
> VIR_DEBUG("Using credentials directory from environment: %s",
> credentials_directory);
>
> cfg->secretsEncryptionKeyPath =
> g_strdup_printf("%s/secrets-encryption-key",
>
> credentials_directory);
> }
>
> VIR_DEBUG("Secrets encryption key path: %s",
> NULLSTR(cfg->secretsEncryptionKeyPath));
>
>
Sure
> > + if (cfg->secretsEncryptionKeyPath &&
> > access(cfg->secretsEncryptionKeyPath, R_OK) == 0) {
> > + if (!getSecretsEncryptionKey(cfg, &cfg->secrets_encryption_key,
> > &cfg->secretsKeyLen)) {
> > + VIR_DEBUG("Failed to get secrets encryption key from path: %s",
> > + cfg->secretsEncryptionKeyPath);
> > + goto error;
>
> You must properly report the error if you are to bail from daemon startup.
> Here !getSecretsEncryptionKey sets the value but it doesn't seem so
> because you do a pointless DEBUG (the error was already reported)
>
I will remove the debug statement.
> > + }
> > + }
> > +
> > + if (cfg->encrypt_data != 1) {
> > + cfg->encrypt_data = (cfg->secretsKeyLen == 32) ? 1 : 0;
>
> So you set this to 1 even when the user set it to 0? Also 'encrypt_data'
> really ought to be a bool in the config struct. If users set this to 0
> explicitly you shouldn't try to re-infer it.
>
> Here you also duplicate a magic constant used to limit the size so if
> anyone ever changes it in the other place only this will stop to work.
>
> If you want to automatically set this you'll also need to remember
> (possibly in another bool) if the explicit config option was seen or
> not.
can we set this to 1 before even reading the config file?
Because we want to encrypt the secrets by default
That way, it can either be changed to 0 or remain 1.
>
> > + } else if (cfg->encrypt_data == 1) {
> > + if (!cfg->secretsEncryptionKeyPath) {
> > + virReportError(VIR_ERR_INTERNAL_ERROR,
> > + _("secretsEncryptionKeyPath must be set if
> > encrypt_data is 1 in %1$s"),
> > + configfile);
>
> This isn't an internal error, it's user's wrong config.
>
> Also make it a standalone check rather than an else branch.
yes will do.
>
> > + goto error;
> > + }
> > + }
> > + return g_steal_pointer(&cfg);
> > + error:
> > + virSecretDaemonConfigDispose(cfg);
>
> cfg is declared as autoptr so this is not needed as it ought to be
> called automatically. This also means that the whole 'error' label can
> be removed and 'NULL returned directly.
>
> > + return NULL;
> > +}
> > +
> > +static void
> > +virSecretDaemonConfigDispose(void *obj)
> > +{
> > + virSecretDaemonConfig *cfg = obj;
> > +
> > + g_free(cfg->secrets_encryption_key);
>
> Use the function to securely clear it first, before freeing it here.
yes, will do.
>
> > + g_free(cfg->secretsEncryptionKeyPath);
> > +}
> > diff --git a/src/conf/secret_config.h b/src/conf/secret_config.h
> > new file mode 100644
> > index 0000000000..638b7c49a4
> > --- /dev/null
> > +++ b/src/conf/secret_config.h
> > @@ -0,0 +1,48 @@
> > +/*
> > + * secret_config.h: secrets.conf config file handling
> > + *
> > + * Copyright (C) 2025 Red Hat, Inc.
> > + *
> > + * This library is free software; you can redistribute it and/or
> > + * modify it under the terms of the GNU Lesser General Public
> > + * License as published by the Free Software Foundation; either
> > + * version 2.1 of the License, or (at your option) any later version.
> > + *
> > + * This library is distributed in the hope that it will be useful,
> > + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> > + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> > + * Lesser General Public License for more details.
> > + *
> > + * You should have received a copy of the GNU Lesser General Public
> > + * License along with this library. If not, see
> > + * <http://www.gnu.org/licenses/>.
> > + */
> > +
> > +#pragma once
> > +
> > +#include "internal.h"
> > +#include "virinhibitor.h"
> > +#include "secret_event.h"
> > +
> > +typedef struct _virSecretDaemonConfig virSecretDaemonConfig;
> > +struct _virSecretDaemonConfig {
> > + virObject parent;
> > + /* secrets encryption key path from secrets.conf file */
> > + char *secretsEncryptionKeyPath;
> > +
> > + unsigned char* secrets_encryption_key;
>
> unsigned char *sec...
Yes.
>
> > + size_t secretsKeyLen;
> > +
> > + /* Indicates if the newly written secrets are encrypted or not.
> > + * 0 if not encrypted and 1 if encrypted.
> > + */
> > + int encrypt_data;
>
> This ought to be either a 'bool' or one of the tristates. Alternatively
> two bools.
Can we keep it a boolean, and by default set it to 1? After reading config, it
will stay 1 or change to 0.
>
> > +};
> > +
> > +G_DEFINE_AUTOPTR_CLEANUP_FUNC(virSecretDaemonConfig, virObjectUnref);
> > +
> > +int virSecretDaemonConfigFilePath(bool privileged, char **configfile);
> > +virSecretDaemonConfig *virSecretDaemonConfigNew(bool privileged);
> > +int virSecretDaemonConfigLoadFile(virSecretDaemonConfig *data,
> > + const char *filename,
> > + bool allow_missing);
> > diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms
>
Regards,
Arun Menon