Skip to content

Keyset locking#139

Merged
Philip-NLnetLabs merged 82 commits intomainfrom
keyset-locking
Mar 2, 2026
Merged

Keyset locking#139
Philip-NLnetLabs merged 82 commits intomainfrom
keyset-locking

Conversation

@Philip-NLnetLabs
Copy link
Member

@Philip-NLnetLabs Philip-NLnetLabs commented Nov 19, 2025

Fixes #150.

Philip-NLnetLabs and others added 30 commits April 17, 2025 13:37
Co-authored-by: Terts Diepraam <terts.diepraam@gmail.com>
- Adds KMIP server based key generation, signing and destruction, equivalent to the existing Ring/OpenSSL functionality.
- Adds new kmip subcommands for managing KMIP server configurations.
- Adds support for referring to KMIP keys by a new KMIP URL scheme. 
- Add a feature for the KMIP crypto backend just like the Ring and OpenSSL crypto backends.
- Adds support for storing sensitive credentials in files separate to the KMIP server configuration.
…port to the proposed channel at packages.nlnetlabs.nl.
* Restructure roll commands.

* Import public keys.

* Import a public/private key pair from files.

* Add a default TTL to config. Use that for DNSKEY/CDS/CDNSKEY/DS RRsets.

* Cargo.lock.

* Support for importing KMIP keys.

* Import public/private keys in decoupled state

* Add --private-key option to importing a public/private key pair from files.

* Add remove-key command.
Philip-NLnetLabs and others added 19 commits November 26, 2025 12:22
#138)

* Introduce a 'WorkSpace' object to keep the current working state of keyset.

* Fallout from merging.

* Switch to the crypto-and-keyset-fixes branch in domain for the time being.
Disable kmip because the branch does not support that.

* Update lock file.

* Bump Rust version to 1.85 because domain is at 1.85.0.

* Switch to domain-kmip. (#142)

* Switch to domain-kmip.

* Use https URL for domain-kmip.

* Update for domain-kmip change.

* Switch to main branch of domain-kmip.

* Clippy.

* Bump Rust version to 1.88 for kmip-protocol.
@Philip-NLnetLabs Philip-NLnetLabs changed the base branch from keyset-improvements to main January 23, 2026 08:54
@ximon18 ximon18 modified the milestone: next Feb 17, 2026
tracing-subscriber = "0.3.19"
url = "2.5.4"
futures = "0.3.31"
fs2 = "0.4.3"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this is a good choice of dependency. What were your reasons for selecting it?
I ask because:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave it at fs2 for now. If fs2 gets less popular or picks up security issues then we can switch. The interface of fd-lock looks scary.

Comment on lines +5705 to +5706
/// locking the file, the function has to check if the locked file is this
/// the current fine under that name.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment has some typos and grammatical issues.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to:

/// Assume changes are saved by creating a new file and renaming. After
/// locking the file, the function has to check if the locked file is the
/// same as the current file under that name.

// The config file is updated by writing to a new file and the renaming.
// Might have locked the old file. Check. Try a number of times and
// then give up. Lock contention is expected to be low.
for _try in 0..MAX_FILE_LOCK_TRIES {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it okay to retry lock acquisition with no delay between attempts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will lead to the thundering herd problem. But concurrent access to keyset for a single domain is expected to be very low. We need to do something if we ever fail to get a lock within MAX_FILE_LOCK_TRIES.

let file = File::open(filename)
.map_err(|e| format!("unable to open file {}: {e}", filename.display()))?;

file.lock_exclusive()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the documentation this call will block if the file is currently locked, is that desirable or would it be better to call try_lock_exclusive() instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this PR the goal is to block. Maybe the UI needs to be improved in the future.

filename.display()
)
})?;
let current_file_handle = Handle::from_path(filename)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You open a file by path, lock it and get its handle, then get another handle for the same path and compare the two handles. Why? What is this handle comparison achieving/intended to do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is explained by the comment at the top of the function. Maybe it needs more text. The idea is that because the config is saved by creating a new file and then renaming that file, it is possible to endup with a lock on the old config file that no longer exists. So check that we have locked to current config file.

Copy link
Member

@ximon18 ximon18 Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only call write_locked_file() once, so any rename that happens within your code when the config file is saved won't be detected later by this logic, at least currently. So is this for the some future potential second call to this function after saving/renaming of the config file?

Also, when this test fails you call continue so you go round the loop again, dropping the file you just opened and the lock you just acquired, and try re-opening the file by the same filename as you just tried, even though that succeeded. I don't understand what this achieves.

If this function were called while a second concurrent invocation of dnst keyset was busy saving the config file, one would be able to open the file then block until the lock can be acquired, is this the issue, that you would then be possibly be opening the old FD? (though what happens to that FD during the rename/once the rename completes, is it still valid?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a rename, any processes that run keyset and that were trying to lock the config file now try to lock the wrong file. So they need to start again. The logic of keyset is that it gets a lock on the config file, and then saves the state file if it changed and saves the config file if it changed. There will not be a second call to lock the config file in a single run of keyset.

///
/// First write to a new filename and then rename to make sure that
/// changes are atomic.
fn write_config(&self, keyset_conf: &PathBuf) -> Result<(), Error> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and fn write_state() look almost identical, I would suggest factoring their core logic out into a single helper function, especially as they both contain the same comment and work around for lacking -fn add_extension() support.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


let mut file = File::create(&conf_file_new)
.map_err(|e| format!("unable to create file {}: {e}", conf_file_new.display()))?;
write!(file, "{json}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If some reason creation, writing or renaming fail, the created .new file will be left behind, right? Is that a problem? Subsequent attempts would fail I assume as the created file already exists. Should you attempt to remove the file in such scenarios?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't expect create to fail if a file with the same name already exists.

/// Assume changes are saved by creating a new file and renaming. After
/// locking the file, the function has to check if the locked file is this
/// the current fine under that name.
fn write_locked_file(filename: &PathBuf) -> Result<File, Error> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the name of this function confusing, as "write" reads like a verb yet this function does neither opens the file for writing nor writes to it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to file_with_write_lock

@Philip-NLnetLabs Philip-NLnetLabs merged commit fdaa43a into main Mar 2, 2026
20 checks passed
@Philip-NLnetLabs Philip-NLnetLabs deleted the keyset-locking branch March 2, 2026 11:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

keyset file locking

4 participants