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

Introduce ability to generate kickstarts #2136

Merged
merged 53 commits into from
Jul 31, 2024

Conversation

jan-cerny
Copy link
Member

@jan-cerny jan-cerny commented Jul 10, 2024

This PR introduces ability to generate kickstarts from SCAP content using:

oscap xccdf generate fix --fix-type kickstart ds.xml

The kickstart will be generated from kickstart snippets in XCCDF rules in the input SCAP content.
The kickstart snippets need to be stored in <fix> elements with system attribute set to urn:xccdf:fix:script:kickstart.

For more details, please read commit messages of each commit.

Rationale:

We'd like to provide a lightweight system hardening for Anaconda, that is not dependent on UI and does not require user intervention. This should be pretty much similar to Image Builder's experience for users that can not use it for some reason.

How to test:

  1. Build RHEL 9 data stream from the ComplianceAsCode pull request master as of 2024-07-29 or newer.
  2. Generate the kickstart ks.cfg:
./oscap_wrapper xccdf generate fix --profile stig --output ks.cfg --fix-type kickstart ~/work/git/scap-security-guide/build/ssg-rhel9-ds.xml
  1. Add AppStream repository to the generated kickstart by adding this line (edit the URL)
repo --name=extra-repository --baseurl=http://DOWNLOAD_HOSTNAME/released/RHEL-9/9.4.0/AppStream/x86_64/os/
  1. Serve the kickstart on a HTTP server
python3 -m http.server
  1. Run virt-manager, click on Create a new virtual machine. Choose Network Install.
  2. Provide installation URL for RHEL 9.4: http://DOWNLOAD_HOSTNAME/released/RHEL-9/9.4.0/BaseOS/x86_64/os/
    Under the installation URL field, click on URL options and insert there inst.ks=http://192.168.124.1:8000/ks.cfg. Complete next steps in the wizard.
  3. VM will launch. Configure partitioning, continue with installation. Watch Anaconda progress.
  4. After installation and reboot, log in to the installed VM and observe state of the system. Read Anaconda logs. Also, you can perform a oscap scan of the installed system.

Review hints:

Start by reading the proposed user manual text in the PR.

This fix type will be used to generate RHEL kickstarts to
support unattended installation of hardened RHEL systems.
If a like starts with packages, services or post, it will be added to
the respective Kickstart section.
You can't scan the machine that you will install using the generated
kickstart (to scan it it would already have to be installed).
Therefore it doesn't make sense to generate kickstarts for results.
For simplicity we will disable this feature.
This header will put some common options that are sensible for
every RHEL kickstart. The purpose to make the installation easier
and set sensible defaults.
The code has been taken from ComplianceAsCode/content RHEL 9 CIS
Kickstart.
We will run an oscap scan in the post phase, therefore
we need to install openscap and scap-security-guide to
be able to run the oscap scan.
Instead of hard-coded "xccdf-file.xml" we will show the real
input SCAP file name in the kenerated remediations.
This will give us the correct file name for most situations.
@comps
Copy link

comps commented Jul 10, 2024

I don't see it currently in the code, but note that an Anaconda kickstart can have multiple %packages sections, so we could have our own, independent of whatever the user wants to add in their own %packages.

This would allow us to use further options to %packages documented on https://pykickstart.readthedocs.io/en/latest/kickstart-docs.html#chapter-9-package-selection , like %packages --exclude-weakdeps (or --excludeWeakdeps to preserve RHEL-8 compatibility) to install the specified packages with minimum dependencies. Maybe something to consider.

Similarly, %post also has

%post [--erroronfail] [--interpreter /usr/bin/python] [--log LOG] [--nochroot]

and I imagine we'd definitely want --erroronfail, and perhaps --nochroot to use the Anaconda environment's tooling rather that the installed system tooling.
https://pykickstart.readthedocs.io/en/latest/kickstart-docs.html#chapter-6-post-installation-script


Also, what about the bootloader command? .. Presumably we don't want it just hardcoded in common_kickstart_header, but rather dynamic, so stuff like fips=1 can be added there (alongside other kernel command line options).

"# --device device to be activated and / or configured with the network command\n"
"# --bootproto method to obtain networking configuration for device (default dhcp)\n"
"# --noipv6 disable IPv6 on this device\n"
"network --onboot yes --device eth0 --bootproto dhcp --noipv6\n"
Copy link

Choose a reason for hiding this comment

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

Just

network --onboot yes --bootproto dhcp

should be enough. The rest is autodetected by Anaconda.

Comment on lines 1534 to 1537
"# Configure firewall settings for the system (optional)\n"
"# --enabled reject incoming connections that are not in response to outbound requests\n"
"# --ssh allow sshd service through the firewall\n"
"firewall --enabled --ssh\n"
Copy link

Choose a reason for hiding this comment

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

I'm pretty sure we can remove this - our testing runs without the firewall command as Anaconda by default installs firewalld, and installing the openssh-server package adds port 22 to it seamlessly.

@comps
Copy link

comps commented Jul 10, 2024

Do we really want a very verbose kickstart file as an output, giving the user many options to customize it (url/cdrom/harddrive), explaining every option?

Why not just point to https://pykickstart.readthedocs.io/en/latest/kickstart-docs.html (or the RHEL version of it, ie. https://docs.redhat.com/en-us/documentation/red_hat_enterprise_linux/8/pdf/system_design_guide/Red_Hat_Enterprise_Linux-8-System_Design_Guide-en-US.pdf - APPENDIX J ) and let the user figure out what they need for their specific setup ?

@jan-cerny
Copy link
Member Author

@comps great ideas! thanks a lot!

char *basename = oscap_basename(dup);
free(dup);
char *oscap_command = oscap_sprintf(
"oscap xccdf eval --remediate --profile '%s' /usr/share/xml/scap/ssg/content/%s\n",
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 work sometimes, but sometimes it won't work. How do we ensure that the file is the same as the file that the user provided to oscap when generating the kickstart? And even a bigger problem, what if the user used a tailoring when generating the kickstart?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is why we need to at least fix this: #771.

Copy link
Member Author

Choose a reason for hiding this comment

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

Amazing! I didn't know that we have issue for that. Great news is that the issue is already "accidentally" fixed in this PR! The headers now contain actual file path of the file used as input to the oscap xccdf generate fix command.

The knowledge of the actual input file path is also reused in this place. Here, I take its base name and concatenate it with the well-known scap-security-guide directory path. This way I achieved that it will work sometimes. The sometimes specifically means that 1. user uses a data stream from the scap-security-guide package to generate the KS and 2. the version of the scap-security-guide package used to generate the KS is the same that will be installed on the installed system and 3. user doesn't use tailoring when generating the KS. If any of the three conditions isn't true then it won't work or will give surprising results. I think that's insufficient therefore I opened this comment.

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 have this proposal how to solve this problem. We will document that the recommended workflow is to serve the SCAP source data stream and the optional XCCDF tailoring file on a web server. Then, the generated kickstart will contain a command to download these files from the server. We will not install scap-security-guide package during the installation. This way it will make sure that the same files are used for both kickstart generation before installation and remediation during installation. The generated kickstart can contain placeholders for the content URI and comments highlighting it. Also, we can have a CLI option that would get propagated to the generated kickstart. The key thing would be to have the workflow well documented.

Copy link
Member Author

Choose a reason for hiding this comment

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

Another proposal has been suggested by @dahaic : The kickstart would explicitly request the specific version of scap-security-guide RPM package to be installed. The package would be fetch from CDN. This way we would ensure that the same data stream is used for the remediation as is used for the kickstart generation. The tailoring file could be inserted in a form of base64 blob or in a form of autotailor command.

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. It'd be nice to actually have just a full path here for the content. We don't care much about non-packaged DSes. It's up to the advanced customer to figure out how they want to fetch, store and use it (we can give an example in the docs, though).
  2. If we can somehow embed tailoring (when --tailoring option is provided along with generate fix) and generate proper KS that would use it without requiring the user to serve it separately — it would be very nice, but let's not concentrate on it before everything else is complete.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we could be able to bring tailoring into the remediation generator context it will help us to also generate self-contained tailored Blueprints.

The generator will consume `logvol` commands and put them
into the output kickstart files. This will be used in rules
for partitions, for example in the `partition_for_var` rule.
Comment on lines 1491 to 1500
"# Initialize (format) all disks (optional)\n"
"zerombr\n"
"\n"
"# The following partition layout scheme assumes disk of size 20GB or larger\n"
"# Modify size of partitions appropriately to reflect actual machine's hardware\n"
"#\n"
"# Remove Linux partitions from the system prior to creating new ones (optional)\n"
"# --linux erase all Linux partitions\n"
"# --initlabel initialize the disk label to the default based on the underlying architecture\n"
"clearpart --linux --initlabel\n"
Copy link

Choose a reason for hiding this comment

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

I would replace this with

zerombr
clearpart --all --initlabel
reqpart

since zerombr + clearpart --linux doesn't make sense, ... and since we definitely want reqpart which adds EFI partition(s) on x86_64, and PReP boot partition on IBM POWER.

Note that reqpart also has --add-boot, which would nicely add /boot for us, but then we couldn't specify --fsoptions for it, so we can't really use 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.

great idea ! thanks

So far, these large blobs have been only copy-pasted from the kickstarts that we have in upstream without any thinking. It's definitely something that we should investigate and change.

Copy link
Contributor

Choose a reason for hiding this comment

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

Default options for /boot are OK, oscap will remediate them in post section.

- network device should be autodetected by Anaconda
- firewalld is added seamlessly to the port 22
- combination zerombr + clearpart --linux doesn't make sense
We will have a dedicated post section for compliance hardening.
This section will fail if oscap command fails.
It helps us prevent partially or wrongly hardened systems.
The intention is to be able to perform the installation
fully automatically without manual intervention during
the installation process. We will add sensible default
values to the kickstart so that Anaconda won't ask
for setting these options.
We will add a fallback partition layout for profiles that don't specify
partitioning layout, for example RHEL 9 PCI-DSS profile.  We need to
specify at least some layout in the kickstart to make the installation
fully automated.
@jan-cerny
Copy link
Member Author

I have changed the %post section to fail if the oscap remediation fails. I have added default values that ensure fully automated installation. The default values include also partition defaults which will be added to the kickstart if the SCAP profile doesn't specify any partition requirement, which means automated installation for these profiles as well.

@comps
Copy link

comps commented Jul 25, 2024

I have changed the %post section to fail if the oscap remediation fails. I have added default values that ensure fully automated installation. The default values include also partition defaults which will be added to the kickstart if the SCAP profile doesn't specify any partition requirement, which means automated installation for these profiles as well.

You might want to read my post more carefully - I mentioned that lang and others are "required" by documentation, but not actually required, and I provided a full example kickstart that works and doesn't need lang or keyboard or timezone, but you added them anyway -- was that intentional?

Also, for profiles without partitions, maybe autopart (default) would be better, ie.

const char *fallback_partition = (
	"# Create partition layout scheme\n"
	"zerombr\n"
	"clearpart --all --initlabel\n"
	"autopart --type=lvm\n"
);

@mildas
Copy link
Contributor

mildas commented Jul 25, 2024

%post --erroronfail
oscap xccdf eval --remediate ...
[ $? -eq 0 -o $? -eq 2 ]
%end

If I got --erroronfail correctly, it will terminate right at the failed command. Thus, if oscap returns 2 (something failed to be remediated or missing remediation), it will terminate installation at that oscap line and won't [ $? -eq 0 -o $? -eq 2 ].
Shouldn't it be rather something like

%post --erroronfail
oscap xccdf eval --remediate ... || [ $? -eq 2 ]
%end

@comps ?

@jan-cerny
Copy link
Member Author

You might want to read my post more carefully - I mentioned that lang and others are "required" by documentation, but not actually required, and I provided a full example kickstart that works and doesn't need lang or keyboard or timezone, but you added them anyway -- was that intentional?

The important thing here is to distinguish between a kickstart that works and kickstart that enables a fully automated installation. Without the options I added today, the kickstart works (it worked even yesterday), but the installation isn't fully automated, because the user needs to configure the options in the Anaconda during installation.

For example, if you remove the "lang" keyword from the kickstart, the Anaconda will ask you which language you want to use during the installation and you have to click and choose one to proceed. But if there are all the options I added, the installation doesn't require any user interaction in the whole process and goes fully automatically.

To simplify the code, we can use the "autopart" command.
@jan-cerny
Copy link
Member Author

I have used the autopart

@comps
Copy link

comps commented Jul 25, 2024

If I got --erroronfail correctly, it will terminate right at the failed command. Thus, if oscap returns 2 (something failed to be remediated or missing remediation), it will terminate installation at that oscap line and won't [ $? -eq 0 -o $? -eq 2 ].

Right, but that doesn't do set -e in the shell. And the shell will ignore the first failure, and exit with whatever exit code the last command returned.

$ bash -c 'ls /nonexistent'; echo $?
ls: cannot access '/nonexistent': No such file or directory
2

--erroronfail simply means "if the shell returned non-0, abort the installation"".

So my version should still work. Yours works too, but I don't think it would gain us anything.

For example, if you remove the "lang" keyword from the kickstart, the Anaconda will ask you which language you want to use during the installation and you have to click and choose one to proceed. But if there are all the options I added, the installation doesn't require any user interaction in the whole process and goes fully automatically.

Hmm, interesting, because for me, it works fully automatically even without them.

I guess it doesn't ultimately matter, but you could replace the options with

text --non-interactive

to force automated install, but either way works.

@mildas
Copy link
Contributor

mildas commented Jul 25, 2024

--erroronfail simply means "if the shell returned non-0, abort the installation"".

Okay, in that case your solution is fine. But then there's assumption that

oscap xccdf eval --remediate ...
[ $? -eq 0 -o $? -eq 2 ]

will be the last 2 commands in %post. Are we sure it will be like that? Will post command remediation add commands BEFORE oscap xccdf eval? And what about customers adding more stuff to %post? Because for that I miss exit 1 there.

@comps
Copy link

comps commented Jul 25, 2024

will be the last 2 commands in %post. Are we sure it will be like that? Will post command remediation add commands BEFORE oscap xccdf eval? And what about customers adding more stuff to %post? Because for that I miss exit 1 there.

A kickstart will typically have more %post sections, all of them run independently in separately executed shells (or --interpreters).

I guess we can change it to

[ $? -eq 0 -o $? -eq 2 ] || exit 1

for extra safety in case someone adds more code to our %post section.

Options for bootloader sometimes aren't in the form 'option=value' and
so they sometimes don't contain '='.  This commit clarifies the
documentation about the bootloader command with regards to this fact.
Also, it adds a simple test for this command.
 * Concatenate 2 strings
 * Convenience wrapper over strncat.
If a line in kickstart remediation starts with `%post`, that line and
all following lines until a line starting with `%end` are considered a
block. Blocks are propagated to the output without any processing.
If an user doesn't provide `--profile`, the default XCCDF profile
is used which is the standard behavior of all oscap xccdf modules.
However, the generated remediation doesn't reflect that. This commit
fixes it, namely the generated "oscap" commands.
@jan-cerny
Copy link
Member Author

I have add exit 1, add ability to consume custom %pre and %post sections, add ability to disable kdump and solved the situation when --profile isn't provided. Then, I have improved the user manual changes.

@jan-cerny jan-cerny marked this pull request as ready for review July 29, 2024 13:24
@jan-cerny
Copy link
Member Author

/packit build

Copy link
Contributor

@evgenyz evgenyz left a comment

Choose a reason for hiding this comment

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

Okay, there is nothing I would like to change immediately, although we might have some adjustments after getting some feedback from stakeholders (and more experiments with CI etc). I consider it good enough for the first implementation.

Thank you!

@evgenyz evgenyz merged commit 5c0235b into OpenSCAP:main Jul 31, 2024
16 of 17 checks passed
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.

4 participants