Skip to content

Commit

Permalink
adds known_hosts attribute to both datasource and resource (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
marshallford authored Aug 9, 2024
1 parent 0c0fb84 commit 9ac3df6
Show file tree
Hide file tree
Showing 16 changed files with 258 additions and 29 deletions.
1 change: 1 addition & 0 deletions docs/data-sources/navigator_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ output "resolv_conf" {
Optional:

- `force_handlers` (Boolean) Run handlers even if a task fails.
- `known_hosts` (List of String) SSH known host entries. Effectively a list of host public keys. Can help protect against man-in-the-middle attacks by verifying the identity of hosts. Ansible variable `ansible_ssh_known_hosts_file` set to path of `known_hosts` file.
- `limit` (List of String) Further limit selected hosts to an additional pattern.
- `private_keys` (Attributes List) SSH private keys used for authentication in addition to the [automatically mounted](https://ansible.readthedocs.io/projects/navigator/faq/#how-do-i-use-my-ssh-keys-with-an-execution-environment) default named keys and SSH agent socket path. (see [below for nested schema](#nestedatt--ansible_options--private_keys))
- `skip_tags` (List of String) Only run plays and tasks whose tags do not match these values.
Expand Down
30 changes: 26 additions & 4 deletions docs/resources/navigator_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ resource "ansible_navigator_run" "existing" {
inventory = file("inventory/baremetal.yaml")
}
# 3. configure ansible behavior with ansible.cfg placed in working directory (see example below)
# 3. configure ansible with ansible.cfg placed in working directory (see example below)
resource "ansible_navigator_run" "working_directory" {
playbook = "..."
inventory = "..."
Expand Down Expand Up @@ -128,8 +128,8 @@ output "playbook_stdout" {
value = join("\n", jsondecode(ansible_navigator_run.artifact_query_stdout.artifact_queries.stdout.result))
}
# 9. specify ssh private keys
resource "tls_private_key" "this" {
# 9. ssh private keys
resource "tls_private_key" "client" {
algorithm = "ED25519"
}
Expand All @@ -140,11 +140,32 @@ resource "ansible_navigator_run" "private_keys" {
private_keys = [
{
name = "example"
data = tls_private_key.this.private_key_openssh
data = tls_private_key.client.private_key_openssh
}
]
}
}
# 10. ssh known hosts
resource "tls_private_key" "server" {
algorithm = "ED25519"
}
resource "ansible_navigator_run" "known_hosts" {
playbook = "..."
inventory = yamlencode({
all = {
vars = {
ansible_ssh_common_args = "-o StrictHostKeyChecking=yes -o UserKnownHostsFile={{ ansible_ssh_known_hosts_file }}"
}
}
})
ansible_options = {
known_hosts = [
"10.0.0.1 ${tls_private_key.server.public_key_openssh}"
]
}
}
```

### Example `ansible.cfg`
Expand Down Expand Up @@ -202,6 +223,7 @@ pipelining=True
Optional:

- `force_handlers` (Boolean) Run handlers even if a task fails.
- `known_hosts` (List of String) SSH known host entries. Effectively a list of host public keys. Can help protect against man-in-the-middle attacks by verifying the identity of hosts. Ansible variable `ansible_ssh_known_hosts_file` set to path of `known_hosts` file.
- `limit` (List of String) Further limit selected hosts to an additional pattern.
- `private_keys` (Attributes List) SSH private keys used for authentication in addition to the [automatically mounted](https://ansible.readthedocs.io/projects/navigator/faq/#how-do-i-use-my-ssh-keys-with-an-execution-environment) default named keys and SSH agent socket path. (see [below for nested schema](#nestedatt--ansible_options--private_keys))
- `skip_tags` (List of String) Only run plays and tasks whose tags do not match these values.
Expand Down
29 changes: 25 additions & 4 deletions examples/resources/ansible_navigator_run/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ resource "ansible_navigator_run" "existing" {
inventory = file("inventory/baremetal.yaml")
}

# 3. configure ansible behavior with ansible.cfg placed in working directory (see example below)
# 3. configure ansible with ansible.cfg placed in working directory (see example below)
resource "ansible_navigator_run" "working_directory" {
playbook = "..."
inventory = "..."
Expand Down Expand Up @@ -113,8 +113,8 @@ output "playbook_stdout" {
value = join("\n", jsondecode(ansible_navigator_run.artifact_query_stdout.artifact_queries.stdout.result))
}

# 9. specify ssh private keys
resource "tls_private_key" "this" {
# 9. ssh private keys
resource "tls_private_key" "client" {
algorithm = "ED25519"
}

Expand All @@ -125,8 +125,29 @@ resource "ansible_navigator_run" "private_keys" {
private_keys = [
{
name = "example"
data = tls_private_key.this.private_key_openssh
data = tls_private_key.client.private_key_openssh
}
]
}
}

# 10. ssh known hosts
resource "tls_private_key" "server" {
algorithm = "ED25519"
}

resource "ansible_navigator_run" "known_hosts" {
playbook = "..."
inventory = yamlencode({
all = {
vars = {
ansible_ssh_common_args = "-o StrictHostKeyChecking=yes -o UserKnownHostsFile={{ ansible_ssh_known_hosts_file }}"
}
}
})
ansible_options = {
known_hosts = [
"10.0.0.1 ${tls_private_key.server.public_key_openssh}"
]
}
}
9 changes: 7 additions & 2 deletions internal/provider/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func testSSHKeygen(t *testing.T) (string, string) {
return fmt.Sprintf("ssh-ed25519 %s", base64.StdEncoding.EncodeToString(publicKey.Marshal())), string(pem.EncodeToMemory(privateKey))
}

func testSSHServer(t *testing.T, publicKey string) int {
func testSSHServer(t *testing.T, clientPublicKey string, serverPrivateKey string) int {
t.Helper()

listener, err := net.Listen("tcp", "localhost:0")
Expand All @@ -147,7 +147,7 @@ func testSSHServer(t *testing.T, publicKey string) int {

err = sshServer.SetOption(
ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
allowed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKey))
allowed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(clientPublicKey))
if err != nil {
t.Fatal(err)
}
Expand All @@ -159,6 +159,11 @@ func testSSHServer(t *testing.T, publicKey string) int {
t.Fatal(err)
}

err = sshServer.SetOption(ssh.HostKeyPEM([]byte(serverPrivateKey)))
if err != nil {
t.Fatal(err)
}

// TODO wait until ready?
go sshServer.Serve(listener) //nolint:errcheck

Expand Down
16 changes: 16 additions & 0 deletions internal/provider/navigator_run_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ func (m NavigatorRunDataSourceModel) Value(ctx context.Context, run *navigatorRu
run.privateKeys = append(run.privateKeys, key)
}

var knownHosts []string
if !optsModel.KnownHosts.IsNull() {
diags.Append(optsModel.KnownHosts.ElementsAs(ctx, &knownHosts, false)...)
}

run.knownHosts = knownHosts

var queriesModel map[string]ArtifactQueryModel
diags.Append(m.ArtifactQueries.ElementsAs(ctx, &queriesModel, false)...)

Expand Down Expand Up @@ -323,6 +330,15 @@ func (d *NavigatorRunDataSource) Schema(ctx context.Context, req datasource.Sche
},
},
},
"known_hosts": schema.ListAttribute{
Description: fmt.Sprintf("SSH known host entries. Effectively a list of host public keys. Can help protect against man-in-the-middle attacks by verifying the identity of hosts. Ansible variable '%s' set to path of 'known_hosts' file.", ansible.SSHKnownHostsFileVar),
MarkdownDescription: fmt.Sprintf("SSH known host entries. Effectively a list of host public keys. Can help protect against man-in-the-middle attacks by verifying the identity of hosts. Ansible variable `%s` set to path of `known_hosts` file.", ansible.SSHKnownHostsFileVar),
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringIsSSHKnownHost()),
},
},
},
},
"timezone": schema.StringAttribute{
Expand Down
11 changes: 8 additions & 3 deletions internal/provider/navigator_run_data_source_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package provider_test

import (
"fmt"
"path/filepath"
"regexp"
"testing"
Expand All @@ -10,6 +11,7 @@ import (
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)

const (
Expand Down Expand Up @@ -171,10 +173,12 @@ func TestAccNavigatorRunDataSource_private_keys(t *testing.T) { //nolint:paralle
variables = test.variables(t)
}

publicKey, privateKey := testSSHKeygen(t)
port := testSSHServer(t, publicKey)
clientPublicKey, clientPrivateKey := testSSHKeygen(t)
serverPublicKey, serverPrivateKey := testSSHKeygen(t)
port := testSSHServer(t, clientPublicKey, serverPrivateKey)

variables["private_key_data"] = config.StringVariable(privateKey)
variables["client_private_key_data"] = config.StringVariable(clientPrivateKey)
variables["server_public_key_data"] = config.StringVariable(serverPublicKey)
variables["ssh_port"] = config.IntegerVariable(port)

resource.Test(t, resource.TestCase{
Expand All @@ -187,6 +191,7 @@ func TestAccNavigatorRunDataSource_private_keys(t *testing.T) { //nolint:paralle
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet(navigatorRunDataSource, "id"),
resource.TestCheckResourceAttrSet(navigatorRunDataSource, "command"),
resource.TestMatchResourceAttr(navigatorRunDataSource, "command", regexp.MustCompile(fmt.Sprintf("--private-key(?s)(.*)--extra-vars %s", ansible.SSHKnownHostsFileVar))),
),
},
},
Expand Down
19 changes: 19 additions & 0 deletions internal/provider/navigator_run_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type AnsibleOptionsModel struct {
Limit types.List `tfsdk:"limit"`
Tags types.List `tfsdk:"tags"`
PrivateKeys types.List `tfsdk:"private_keys"`
KnownHosts types.List `tfsdk:"known_hosts"`
}

type PrivateKeyModel struct {
Expand Down Expand Up @@ -192,6 +193,8 @@ func (m AnsibleOptionsModel) Value(ctx context.Context, options *ansible.Options
}
options.PrivateKeys = privateKeys

options.KnownHosts = len(m.KnownHosts.Elements()) > 0

return diags
}

Expand Down Expand Up @@ -263,6 +266,13 @@ func (m NavigatorRunResourceModel) Value(ctx context.Context, run *navigatorRun,
run.privateKeys = append(run.privateKeys, key)
}

var knownHosts []string
if !optsModel.KnownHosts.IsNull() {
diags.Append(optsModel.KnownHosts.ElementsAs(ctx, &knownHosts, false)...)
}

run.knownHosts = knownHosts

var queriesModel map[string]ArtifactQueryModel
diags.Append(m.ArtifactQueries.ElementsAs(ctx, &queriesModel, false)...)

Expand Down Expand Up @@ -471,6 +481,15 @@ func (r *NavigatorRunResource) Schema(ctx context.Context, req resource.SchemaRe
},
},
},
"known_hosts": schema.ListAttribute{
Description: fmt.Sprintf("SSH known host entries. Effectively a list of host public keys. Can help protect against man-in-the-middle attacks by verifying the identity of hosts. Ansible variable '%s' set to path of 'known_hosts' file.", ansible.SSHKnownHostsFileVar),
MarkdownDescription: fmt.Sprintf("SSH known host entries. Effectively a list of host public keys. Can help protect against man-in-the-middle attacks by verifying the identity of hosts. Ansible variable `%s` set to path of `known_hosts` file.", ansible.SSHKnownHostsFileVar),
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(stringIsSSHKnownHost()),
},
},
},
},
"timezone": schema.StringAttribute{
Expand Down
4 changes: 4 additions & 0 deletions internal/provider/navigator_run_resource_errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ func TestAccNavigatorRunResource_errors(t *testing.T) {
name: "env_var_name_invalid",
expected: regexp.MustCompile("must consist only of printable ASCII characters"),
},
{
name: "known_hosts",
expected: regexp.MustCompile("must not be empty(?s)(.*)illegal base64 data(?s)(.*)multiple known host entries"),
},
{
name: "navigator_preflight",
variables: func(t *testing.T) config.Variables { //nolint:thelper
Expand Down
11 changes: 8 additions & 3 deletions internal/provider/navigator_run_resource_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package provider_test

import (
"fmt"
"path/filepath"
"regexp"
"testing"
Expand All @@ -12,6 +13,7 @@ import (
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
"github.com/marshallford/terraform-provider-ansible/pkg/ansible"
)

const (
Expand Down Expand Up @@ -235,10 +237,12 @@ func TestAccNavigatorRunResource_private_keys(t *testing.T) { //nolint:parallelt
variables = test.variables(t)
}

publicKey, privateKey := testSSHKeygen(t)
port := testSSHServer(t, publicKey)
clientPublicKey, clientPrivateKey := testSSHKeygen(t)
serverPublicKey, serverPrivateKey := testSSHKeygen(t)
port := testSSHServer(t, clientPublicKey, serverPrivateKey)

variables["private_key_data"] = config.StringVariable(privateKey)
variables["client_private_key_data"] = config.StringVariable(clientPrivateKey)
variables["server_public_key_data"] = config.StringVariable(serverPublicKey)
variables["ssh_port"] = config.IntegerVariable(port)

resource.Test(t, resource.TestCase{
Expand All @@ -251,6 +255,7 @@ func TestAccNavigatorRunResource_private_keys(t *testing.T) { //nolint:parallelt
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet(navigatorRunResource, "id"),
resource.TestCheckResourceAttrSet(navigatorRunResource, "command"),
resource.TestMatchResourceAttr(navigatorRunResource, "command", regexp.MustCompile(fmt.Sprintf("--private-key(?s)(.*)--extra-vars %s", ansible.SSHKnownHostsFileVar))),
),
},
},
Expand Down
14 changes: 11 additions & 3 deletions internal/provider/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,12 @@ type navigatorRun struct {
options ansible.Options
navigatorSettings ansible.NavigatorSettings
privateKeys []ansible.PrivateKey
knownHosts []ansible.KnownHost
artifactQueries map[string]ansible.ArtifactQuery
command string
}

func run(ctx context.Context, diags *diag.Diagnostics, timeout time.Duration, operation terraformOperation, run *navigatorRun) {
func run(ctx context.Context, diags *diag.Diagnostics, timeout time.Duration, operation terraformOperation, run *navigatorRun) { //nolint:cyclop
var err error

ctx = tflog.SetField(ctx, "dir", run.dir)
Expand Down Expand Up @@ -118,8 +119,15 @@ func run(ctx context.Context, diags *diag.Diagnostics, timeout time.Duration, op
err = ansible.CreateInventoryFile(run.dir, run.inventory)
addError(diags, "Ansible inventory file not created", err)

err = ansible.CreatePrivateKeys(run.dir, run.privateKeys, &run.navigatorSettings)
addError(diags, "Private keys not created", err)
if len(run.privateKeys) > 0 {
err = ansible.CreatePrivateKeys(run.dir, run.privateKeys, &run.navigatorSettings)
addError(diags, "Private keys not created", err)
}

if len(run.knownHosts) > 0 {
err = ansible.CreateKnownHosts(run.dir, run.knownHosts, &run.navigatorSettings)
addError(diags, "Known hosts not created", err)
}

run.navigatorSettings.EnvironmentVariablesSet[navigatorRunOperationEnvVar] = operation.String()
run.navigatorSettings.Timeout = timeout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ data "ansible_navigator_run" "test" {
test = {
ansible_host = "127.0.0.1"
ansible_port = var.ssh_port
ansible_ssh_common_args = "-o UserKnownHostsFile=/dev/null"
ansible_ssh_common_args = "-o StrictHostKeyChecking=yes -o UserKnownHostsFile={{ ansible_ssh_known_hosts_file }}"
}
}
}
Expand All @@ -31,8 +31,11 @@ data "ansible_navigator_run" "test" {
private_keys = [
{
name = "test"
data = var.private_key_data
}
data = var.client_private_key_data
},
]
known_hosts = [
"[127.0.0.1]:${var.ssh_port} ${var.server_public_key_data}",
]
}
}
Expand All @@ -47,7 +50,12 @@ variable "ee_enabled" {
nullable = false
}

variable "private_key_data" {
variable "client_private_key_data" {
type = string
nullable = false
}

variable "server_public_key_data" {
type = string
nullable = false
}
Expand Down
Loading

0 comments on commit 9ac3df6

Please sign in to comment.