Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for trust tokens #599

Merged
merged 11 commits into from
Jul 17, 2024
10 changes: 8 additions & 2 deletions contrib_testing/local-http-test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3

import datetime
import os
import time

import requests
Expand All @@ -12,7 +13,7 @@


def log(s):
now = datetime.datetime.utcnow()
now = datetime.datetime.now(datetime.UTC)
print(f"{now} - {s}")


Expand Down Expand Up @@ -49,6 +50,11 @@ def create_and_update(client):
if __name__ == "__main__":
client = pylxd.Client("https://127.0.0.1:8443/", verify=False)
log("Authenticating...")
client.authenticate("password")
if client.has_api_extension("explicit_trust_token"):
secret = os.getenv("LXD_TOKEN")
else:
secret = "password"

client.authenticate(secret)

create_and_update(client)
10 changes: 8 additions & 2 deletions contrib_testing/local-unix-test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3

import datetime
import os
import time

import requests
Expand All @@ -12,7 +13,7 @@


def log(s):
now = datetime.datetime.utcnow()
now = datetime.datetime.now(datetime.UTC)
print(f"{now} - {s}")


Expand Down Expand Up @@ -49,6 +50,11 @@ def create_and_update(client):
if __name__ == "__main__":
client = pylxd.Client()
log("Authenticating...")
client.authenticate("password")
if client.has_api_extension("explicit_trust_token"):
secret = os.getenv("LXD_TOKEN")
else:
secret = "password"

client.authenticate(secret)

create_and_update(client)
10 changes: 8 additions & 2 deletions contrib_testing/remote-test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3

import datetime
import os
import time

import requests
Expand All @@ -12,7 +13,7 @@


def log(s):
now = datetime.datetime.utcnow()
now = datetime.datetime.now(datetime.UTC)
print(f"{now} - {s}")


Expand Down Expand Up @@ -50,6 +51,11 @@ def create_and_update(client):
if __name__ == "__main__":
client = pylxd.Client("https://10.245.162.33:8443/", verify=False)
log("Authenticating...")
client.authenticate("password")
if client.has_api_extension("explicit_trust_token"):
secret = os.getenv("LXD_TOKEN")
else:
secret = "password"

client.authenticate(secret)

create_and_update(client)
11 changes: 6 additions & 5 deletions doc/source/authentication.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ Client Authentication
=====================

When using LXD over https, LXD uses an asymmetric keypair for authentication.
The keypairs are added to the authentication database after entering the LXD
instance's "trust password".
The keypairs are added to the authentication database after entering a secret.
The secret can be the LXD trust password, when using LXD 5.0 or older, or a
trust token otherwise.


Generate a certificate
Expand Down Expand Up @@ -35,11 +36,11 @@ essentially meaning that the authentication has not yet occurred.
>>> client.trusted
False

In order to authenticate the client, pass the lxd instance's trust
password to `Client.authenticate`
In order to authenticate the client, pass the LXD instance's trust
password or token to `Client.authenticate`

.. code-block:: python

>>> client.authenticate('a-secret-trust-password')
>>> client.authenticate('a-secret')
>>> client.trusted
>>> True
7 changes: 4 additions & 3 deletions doc/source/certificates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ methods:

- `all()` - Retrieve all certificates.
- `get()` - Get a specifit certificate, by its fingerprint.
- `create()` - Create a new certificate. This method requires
a first argument that is the LXD trust password, and the cert
data, in binary format.
- `create()` - Create a new certificate. This method requires a first argument
that is a secret and a second containing the cert data, in binary format.
The secret can be the LXD trust password, when using LXD 5.0 or older,
or a trust token otherwise.


Certificate attributes
Expand Down
7 changes: 5 additions & 2 deletions integration/run-integration-tests
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ fi
# Make sure a client.{crt,key} exist by trying to add a bogus remote
lxc remote add foo 127.0.0.1:1234 2>/dev/null || true

if ! lxc info | grep -qwF explicit_trust_token; then
lxc config set core.https_address 127.0.0.1
if lxc info | grep -qwF explicit_trust_token; then
LXD_TOKEN="$(lxc config trust add --name pylxd --quiet)"
export LXD_TOKEN
else
lxc config set core.trust_password password
fi
lxc config set core.https_address 127.0.0.1

if ! lxc storage show default >/dev/null 2>&1; then
lxc storage create default dir
Expand Down
21 changes: 14 additions & 7 deletions integration/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.

import os

import pylxd
from integration.testing import IntegrationTestCase
from pylxd import exceptions
Expand All @@ -21,16 +23,16 @@ class TestClient(IntegrationTestCase):
"""Tests for `Client`."""

def test_authenticate(self):
if self.client.has_api_extension("explicit_trust_token"):
self.skipTest(
"Required LXD support for password authentication not available!"
)

client = pylxd.Client("https://127.0.0.1:8443/")

if client.has_api_extension("explicit_trust_token"):
secret = os.getenv("LXD_TOKEN")
else:
secret = "password"

self.assertFalse(client.trusted)

client.authenticate("password")
client.authenticate(secret)

self.assertTrue(client.trusted)

Expand All @@ -48,5 +50,10 @@ def test_authenticate_with_project(self):
self.skipTest(message)
raise

client.authenticate("password")
if client.has_api_extension("explicit_trust_token"):
secret = os.getenv("LXD_TOKEN")
else:
secret = "password"

client.authenticate(secret)
self.assertEqual(client.host_info["environment"]["project"], "test-project")
14 changes: 12 additions & 2 deletions migration/run_migration_integration_tests-18-04
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@ lxc start "$CONTAINER_ONE_NAME"
sleep 5 # Wait for the network to come up
lxc exec "$CONTAINER_ONE_NAME" -- apt update
lxc exec "$CONTAINER_ONE_NAME" -- apt install -y tox python3-dev libssl-dev libffi-dev build-essential criu
lxc exec "$CONTAINER_ONE_NAME" -- lxc config set core.trust_password password
lxc exec "$CONTAINER_ONE_NAME" -- lxc config set core.https_address "[::]"
if lxc exec "$CONTAINER_ONE_NAME" -- lxc info | grep -qwF explicit_trust_token; then
LXD_TOKEN_ONE="$(lxc exec "$CONTAINER_ONE_NAME" -- lxc config trust add --name pylxd --quiet)"
export LXD_TOKEN_ONE
else
lxc exec "$CONTAINER_ONE_NAME" -- lxc config set core.trust_password password
fi
lxc exec "$CONTAINER_ONE_NAME" -- mkdir -p /root/.config/lxc
lxc exec "$CONTAINER_ONE_NAME" -- openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 \
-sha384 -keyout /root/.config/lxc/client.key -out /root/.config/lxc/client.crt -nodes \
Expand All @@ -51,8 +56,13 @@ lxc start "$CONTAINER_TWO_NAME"
sleep 5 # Wait for the network to come up
lxc exec "$CONTAINER_TWO_NAME" -- apt update
lxc exec "$CONTAINER_TWO_NAME" -- apt install -y tox python3-dev libssl-dev libffi-dev build-essential criu
lxc exec "$CONTAINER_TWO_NAME" -- lxc config set core.trust_password password
lxc exec "$CONTAINER_TWO_NAME" -- lxc config set core.https_address "[::]:8443"
if lxc exec "$CONTAINER_TWO_NAME" -- lxc info | grep -qwF explicit_trust_token; then
LXD_TOKEN_TWO="$(lxc exec "$CONTAINER_TWO_NAME" -- lxc config trust add --name pylxd --quiet)"
export LXD_TOKEN_TWO
else
lxc exec "$CONTAINER_TWO_NAME" -- lxc config set core.trust_password password
fi
lxc exec "$CONTAINER_ONE_NAME" -- mkdir -p /root/.config/lxc
lxc exec "$CONTAINER_TWO_NAME" -- openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 \
-sha384 -keyout /root/.config/lxc/client.key -out /root/.config/lxc/client.crt -nodes \
Expand Down
37 changes: 32 additions & 5 deletions migration/test_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.

import os

from integration.testing import IntegrationTestCase


Expand All @@ -35,10 +37,20 @@ def test_migrate_running(self):
second_host = "https://10.0.3.222:8443/"

client1 = Client(endpoint=first_host, verify=False)
client1.authenticate("password")
if client1.has_api_extension("explicit_trust_token"):
secret = os.getenv("LXD_TOKEN_ONE")
else:
secret = "password"

client1.authenticate(secret)

client2 = Client(endpoint=second_host, verify=False)
client2.authenticate("password")
if client2.has_api_extension("explicit_trust_token"):
secret = os.getenv("LXD_TOKEN_TWO")
else:
secret = "password"

client2.authenticate(secret)
an_container = client1.containers.get(self.container.name)
an_container.start(wait=True)
an_container.sync()
Expand All @@ -53,7 +65,12 @@ def test_migrate_local_client(self):

second_host = "https://10.0.3.222:8443/"
client2 = Client(endpoint=second_host, verify=False)
client2.authenticate("password")
if client2.has_api_extension("explicit_trust_token"):
secret = os.getenv("LXD_TOKEN_TWO")
else:
secret = "password"

client2.authenticate(secret)

self.assertRaises(ValueError, self.container.migrate, client2)

Expand All @@ -65,10 +82,20 @@ def test_migrate_stopped(self):
second_host = "https://10.0.3.222:8443/"

client1 = Client(endpoint=first_host, verify=False)
client1.authenticate("password")
if client1.has_api_extension("explicit_trust_token"):
secret = os.getenv("LXD_TOKEN_ONE")
else:
secret = "password"

client1.authenticate(secret)

client2 = Client(endpoint=second_host, verify=False)
client2.authenticate("password")
if client2.has_api_extension("explicit_trust_token"):
secret = os.getenv("LXD_TOKEN_TWO")
else:
secret = "password"

client2.authenticate(secret)
an_container = client1.containers.get(self.container.name)
an_migrated_container = an_container.migrate(client2, wait=True)

Expand Down
4 changes: 2 additions & 2 deletions pylxd/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,11 +478,11 @@ def assert_has_api_extension(self, name):
if not self.has_api_extension(name):
raise exceptions.LXDAPIExtensionNotAvailable(name)

def authenticate(self, password):
def authenticate(self, secret):
if self.trusted:
return
cert = open(self.api.session.cert[0]).read().encode("utf-8")
self.certificates.create(password, cert)
self.certificates.create(secret, cert)

# Refresh the host info
response = self.api.get()
Expand Down
7 changes: 5 additions & 2 deletions pylxd/models/certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def all(cls, client):
def create(
cls,
client,
password,
secret,
cert_data,
cert_type="client",
name="",
Expand All @@ -68,11 +68,14 @@ def create(
data = {
"type": cert_type,
"certificate": base64_cert,
"password": password,
"name": name,
"restricted": restricted,
"projects": projects,
}
if client.has_api_extension("explicit_trust_token"):
data["trust_token"] = secret
else:
data["password"] = secret
response = client.api.certificates.post(json=data)
location = response.headers["Location"]
fingerprint = location.split("/")[-1]
Expand Down
7 changes: 6 additions & 1 deletion pylxd/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,12 @@ def test_authenticate(self):
"""A client is authenticated."""
response = mock.MagicMock(status_code=200)
response.json.side_effect = [
{"metadata": {"auth": "untrusted"}},
{
"metadata": {
"auth": "untrusted",
"api_extensions": ["explicit_trust_token"],
}
},
{
"metadata": {
"type": "client",
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ commands =
pytest {posargs:pylxd}

[testenv:integration]
passenv = LXD_*
commands =
pytest integration {posargs}

Expand Down